diff --git a/node/eslint.config.js b/node/eslint.config.js index 7c4cecb..0c3d7b4 100644 --- a/node/eslint.config.js +++ b/node/eslint.config.js @@ -2,13 +2,23 @@ import js from '@eslint/js' import globals from 'globals' import tseslint from 'typescript-eslint' import { defineConfig } from 'eslint/config' +import simpleImportSort from 'eslint-plugin-simple-import-sort' export default defineConfig([ { files: ['**/*.ts'], - plugins: { js }, + plugins: { js, 'simple-import-sort': simpleImportSort }, extends: ['js/recommended'], - languageOptions: { globals: { ...globals.browser, ...globals.node } }, + languageOptions: { + globals: { ...globals.browser, ...globals.node }, + parserOptions: { + projectService: true, + }, + }, + rules: { + 'simple-import-sort/imports': 'error', + 'simple-import-sort/exports': 'error', + }, }, - tseslint.configs.recommended, + tseslint.configs.recommendedTypeChecked, ]) diff --git a/node/src/builders/collection.ts b/node/src/builders/collection.ts new file mode 100644 index 0000000..c160383 --- /dev/null +++ b/node/src/builders/collection.ts @@ -0,0 +1,79 @@ +import { CollectionFlagSet } from '~/flagsets' +import { + applyDeclarations, + FlagWithValue, + FlagWithValueOrOrdinal, + ListOfFlagsWithValue, + ListOfFlagsWithValueOrOrdinal, + NamedFlagWithValue, + NamedFlagWithValueOrOrdinal, +} from './declarative' +import { FlagDefinition } from '~/definitions' +import { + DefineWithValueOrOrdinal, + RequireParentsOrDefineWithValueOrOrdinal, + WithValueOrOrdinal, + WithValueOrOrdinalOrCompose, +} from './syntax' +import { FlagSetBuilder } from './generic' +import { CollectionFlagDefinitionFactory } from '~/definitions/collection' + +export class CollectionFlagSetBuilder + implements + WithValueOrOrdinalOrCompose, CollectionFlagSet>, + RequireParentsOrDefineWithValueOrOrdinal, CollectionFlagSet> +{ + private readonly _underlying: FlagSetBuilder + + public constructor() { + this._underlying = new FlagSetBuilder() + } + + public define(): WithValueOrOrdinal, CollectionFlagSet> + public define(alias: string): WithValueOrOrdinalOrCompose, CollectionFlagSet> + public define(alias?: string): WithValueOrOrdinalOrCompose, CollectionFlagSet> { + this._underlying.define(alias) + return this + } + + public compose(...flags: string[]): DefineWithValueOrOrdinal, CollectionFlagSet> { + this._underlying.compose(flags) + return this + } + + public withValue( + value: T, + ): RequireParentsOrDefineWithValueOrOrdinal, CollectionFlagSet> { + this._underlying.withValue(value) + return this + } + + public withOrdinal( + ordinal: number, + ): RequireParentsOrDefineWithValueOrOrdinal, CollectionFlagSet> { + throw new Error('Method not implemented.') + } + + public requires(...flags: string[]): DefineWithValueOrOrdinal, CollectionFlagSet> { + this._underlying.requires(flags) + return this + } + + public getResult(): CollectionFlagSet { + const graph = this._underlying.finish() + const factory = new CollectionFlagDefinitionFactory() + return new CollectionFlagSet(graph.intoDictionary(factory)) + } +} + +export function createCollectionFlagSet(declarations: FlagWithValue[]): CollectionFlagSet +export function createCollectionFlagSet( + declarations: Record>, +): CollectionFlagSet & Record>> +export function createCollectionFlagSet( + declarations: ListOfFlagsWithValue, +): CollectionFlagSet { + const builder = new CollectionFlagSetBuilder() + applyDeclarations(declarations, builder) + return builder.getResult() +} diff --git a/node/src/builders/index.ts b/node/src/builders/index.ts index c4a143d..d56c462 100644 --- a/node/src/builders/index.ts +++ b/node/src/builders/index.ts @@ -1 +1,2 @@ export { BitFlagSetBuilder, createBitFlagSet } from './number' +export { createCollectionFlagSet, CollectionFlagSetBuilder } from './collection' diff --git a/node/src/builders/number.ts b/node/src/builders/number.ts index a1e6049..c313036 100644 --- a/node/src/builders/number.ts +++ b/node/src/builders/number.ts @@ -57,7 +57,6 @@ export class BitFlagSetBuilder public getResult(): BitFlagSet { const graph = this._underlying.finish() - const definitions = graph.sortedDefinitions() const factory = new BitFlagDefinitionFactory() return new BitFlagSet(graph.intoDictionary(factory)) } @@ -66,7 +65,7 @@ export class BitFlagSetBuilder export function createBitFlagSet(declarations: FlagWithValueOrOrdinal[]): BitFlagSet export function createBitFlagSet( declarations: Record>, -): BitFlagSet & Record> +): BitFlagSet & Record> export function createBitFlagSet(declarations: ListOfFlagsWithValueOrOrdinal): BitFlagSet { const builder = new BitFlagSetBuilder() applyDeclarations(declarations, builder) diff --git a/node/src/definitions/collection.ts b/node/src/definitions/collection.ts new file mode 100644 index 0000000..3776484 --- /dev/null +++ b/node/src/definitions/collection.ts @@ -0,0 +1,104 @@ +import { FlagDefinition, FlagDefinitionFactory, PartialFlagDefinition } from '.' +import { ENV_SET } from '~/env' + +export class CollectionFlagDefinition implements FlagDefinition> { + private readonly _value: T | undefined + private readonly _alias: string | undefined + private readonly _parents: CollectionFlagDefinition[] + private readonly _children: CollectionFlagDefinition[] + + public constructor( + value: T | undefined, + alias: string | undefined, + parents: CollectionFlagDefinition[], + ) { + this._value = value + this._alias = alias + this._parents = parents + this._children = [] + + for (const parent of this._parents) { + parent._children.push(this) + } + } + + public get alias(): string | undefined { + return this._alias + } + + public get values(): Set { + let values = new Set() + if (this._value === undefined) { + for (const parent of this._parents) { + values = ENV_SET.union.call(values, parent.values) as Set + } + } else { + values.add(this._value) + } + return values + } + + public isIn(set: Set): boolean { + let result = this._value === undefined || set.has(this._value) + for (const parent of this._parents) { + result &&= parent.isIn(set) + } + return result + } + + public addTo(set: Set): Set { + const result = new Set(set) + if (this._value !== undefined) { + result.add(this._value) + } + for (const parent of this._parents) { + parent.addToMutable(result) + } + return result + } + + private addToMutable(set: Set): void { + if (this._value !== undefined) { + set.add(this._value) + } + for (const parent of this._parents) { + parent.addToMutable(set) + } + } + + public removeFrom(set: Set): Set { + const result = new Set(set) + if (this._value !== undefined) { + result.delete(this._value) + } + for (const parent of this._parents) { + parent.removeFromMutable(result) + } + return result + } + + public removeFromMutable(set: Set): void { + if (this._value !== undefined) { + set.delete(this._value) + } + for (const child of this._children) { + child.removeFromMutable(set) + } + } +} + +export class CollectionFlagDefinitionFactory implements FlagDefinitionFactory> { + public makeDefinitions( + sortedPartialDefinitions: PartialFlagDefinition[], + results: Map, FlagDefinition>>, + ): void { + for (const pfd of sortedPartialDefinitions) { + const parents: CollectionFlagDefinition[] = [] + for (const parentPfd of pfd.parents) { + parents.push(results.get(parentPfd) as CollectionFlagDefinition) + } + + results.set(pfd, new CollectionFlagDefinition(pfd.value, pfd.alias, parents)) + } + } +} diff --git a/node/src/definitions/dictionary.ts b/node/src/definitions/dictionary.ts index d6c3834..3250d7f 100644 --- a/node/src/definitions/dictionary.ts +++ b/node/src/definitions/dictionary.ts @@ -1,10 +1,10 @@ export interface FlagsDictionary { - findByAlias(alias: string): FlagDefinition | undefined + findByAlias(alias: string): FlagDefinition | undefined - findByValue(value: F): FlagDefinition | undefined + findByValue(value: F): FlagDefinition | undefined } -export interface FlagDefinition { +export interface FlagDefinition { /** * The alias of the flag. */ @@ -39,9 +39,9 @@ export interface FlagDefinition { export const valueToString = Symbol() -export function printFlagValue(flag: FlagDefinition): string { +export function printFlagValue(flag: FlagDefinition): string { if (valueToString in flag) { - return (flag[valueToString] as Function)() + return (flag[valueToString] as () => string)() } else { return String(flag.values) } diff --git a/node/src/definitions/number.ts b/node/src/definitions/number.ts index bdbe98e..136929c 100644 --- a/node/src/definitions/number.ts +++ b/node/src/definitions/number.ts @@ -6,7 +6,7 @@ interface PrecomputedValues { subtractive: number } -export class BitFlagDefinition implements FlagDefinition { +export class BitFlagDefinition implements FlagDefinition { private readonly _baseValue: number private readonly _additiveValue: number private readonly _subtractiveValue: number @@ -43,7 +43,7 @@ export class BitFlagDefinition implements FlagDefinition { export class BitFlagDefinitionFactory implements FlagDefinitionFactory { public makeDefinitions( sortedPartialDefinitions: PartialFlagDefinition[], - results: Map, FlagDefinition>, + results: Map, FlagDefinition>, ): void { const precomputedValues = new Map, PrecomputedValues>() diff --git a/node/src/env.ts b/node/src/env.ts index d1455d6..18bf332 100644 --- a/node/src/env.ts +++ b/node/src/env.ts @@ -11,18 +11,12 @@ export const ENV_BI = Object.freeze( type SetBinaryOperation = (this: Set, other: Set) => Set type SetBinaryPredicate = (this: Set, other: Set) => boolean -export const ENV_SET = Object.freeze( - typeof Set === 'function' - ? { - AVAILABLE: true, - union: polyfillUnion(Set.prototype), - intersection: polyfillIntersection(Set.prototype), - difference: polyfillDifference(Set.prototype), - isSupersetOf: polyfillIsSupersetOf(Set.prototype), - } - : { AVAILABLE: false }, -) as { - readonly AVAILABLE: boolean +export const ENV_SET = Object.freeze({ + union: polyfillUnion(Set.prototype), + intersection: polyfillIntersection(Set.prototype), + difference: polyfillDifference(Set.prototype), + isSupersetOf: polyfillIsSupersetOf(Set.prototype), +}) as { readonly union: SetBinaryOperation readonly intersection: SetBinaryOperation readonly difference: SetBinaryOperation diff --git a/node/src/flagsets/collection.ts b/node/src/flagsets/collection.ts index 1199dcb..b73879b 100644 --- a/node/src/flagsets/collection.ts +++ b/node/src/flagsets/collection.ts @@ -1,23 +1,35 @@ +import { FlagDefinition, FlagsDictionary } from '~/definitions' +import { EnumerateFlags } from '~/enumeration' +import { ENV_SET } from '~/env' + import type { FlagSet } from '.' -import { EnumerateFlags, useIterator } from '../enumeration' -import { ENV_SET } from '../env' -import { UnavailableFeatureError } from '../errors' export class CollectionFlagSet implements FlagSet> { - /** - * Creates a new empty flag set. - * - * @throws UnavailableFeatureError When this constructor is called in an - * environment that does not natively support {@link Set}s. - */ - public constructor() { - if (!ENV_SET.AVAILABLE) { - throw new UnavailableFeatureError('Sets') - } + private readonly _dictionary: FlagsDictionary> + + public constructor(dictionary: FlagsDictionary>) { + this._dictionary = dictionary } public none(): Set { - return new Set() + return new Set() + } + + public of(...values: T[]): Set { + return new Set(values) + } + + public named(...aliases: string[]): Set { + const result = new Set() + for (const alias of aliases) { + const definition = this.getFlag(alias) + if (definition !== undefined) { + for (const value of definition.values) { + result.add(value) + } + } + } + return result } public union(first: Set, second: Set): Set { @@ -36,18 +48,57 @@ export class CollectionFlagSet implements FlagSet> { return ENV_SET.isSupersetOf.call(first, second) } + public hasAny(flags: Set, required: Set): boolean { + let result = false + for (const value of required) { + const definition = this._dictionary.findByValue(value) + if (definition !== undefined && definition.isIn(flags)) { + result = true + break + } + } + return result + } + + public hasAll(flags: Set, required: Set): boolean { + let result = true + for (const value of required) { + const definition = this._dictionary.findByValue(value) + if (definition !== undefined && !definition.isIn(flags)) { + result = false + break + } + } + return result + } + public enumerate(flags: Set): EnumerateFlags { return flags } - minimum(flags: Set): Set { - throw new Error('Method not implemented.') - } - maximum(flags: Set): Set { - throw new Error('Method not implemented.') + public maximum(flags: Set): Set { + let result = new Set() + for (const value of flags) { + const definition = this._dictionary.findByValue(value) + if (definition !== undefined) { + result = definition.addTo(result) + } + } + return result } - public getFlag(alias: string): FlagDefinition | undefined { - return this._dictionary.lookUp(alias) + public minimum(flags: Set): Set { + let result = new Set() + for (const value of flags) { + const definition = this._dictionary.findByValue(value) + if (definition !== undefined && definition.isIn(flags)) { + result = definition.addTo(result) + } + } + return result + } + + public getFlag(alias: string): FlagDefinition> | undefined { + return this._dictionary.findByAlias(alias) } } diff --git a/node/src/flagsets/number.ts b/node/src/flagsets/number.ts index 0172f13..c8e4e77 100644 --- a/node/src/flagsets/number.ts +++ b/node/src/flagsets/number.ts @@ -91,28 +91,28 @@ export class BitFlagSet implements FlagSet { } public maximum(flags: number): number { - let result = this.none() + let result = 0 for (const value of this.enumerate(flags)) { - const flag = this._dictionary.findByValue(value) - if (flag !== undefined) { - result = flag.addTo(result) + const definition = this._dictionary.findByValue(value) + if (definition !== undefined) { + result = definition.addTo(result) } } return result } public minimum(flags: number): number { - let result = this.none() + let result = 0 for (const value of this.enumerate(flags)) { - const flag = this._dictionary.findByValue(value) - if (flag !== undefined && flag.isIn(flags)) { - result = flag.addTo(result) + const definition = this._dictionary.findByValue(value) + if (definition !== undefined && definition.isIn(flags)) { + result = definition.addTo(result) } } return result } - public getFlag(alias: string): FlagDefinition | undefined { + public getFlag(alias: string): FlagDefinition | undefined { return this._dictionary.findByAlias(alias) } } diff --git a/node/src/index.ts b/node/src/index.ts index ed16209..8fdb435 100644 --- a/node/src/index.ts +++ b/node/src/index.ts @@ -1,4 +1,4 @@ -export { createBitFlagSet } from './builders' +export { createBitFlagSet, createCollectionFlagSet } from './builders' export { FlagDefinition } from './definitions' export { InvalidBitFlagValueError } from './errors' export { diff --git a/node/tests/flagsets/collection.test.ts b/node/tests/flagsets/collection.test.ts index e2964a9..6359ab5 100644 --- a/node/tests/flagsets/collection.test.ts +++ b/node/tests/flagsets/collection.test.ts @@ -1,19 +1,41 @@ -import { CollectionFlagSet } from '~' import { describe, expect, test } from 'vitest' +import { CollectionFlagSet, createCollectionFlagSet } from '~' + function set(...values: T[]): Set { return new Set(values) } describe(CollectionFlagSet, () => { test('none', () => { - const flags = new CollectionFlagSet() + const flags = createCollectionFlagSet([]) expect(flags.none()).toEqual(set()) }) + test('of', () => { + const flags = createCollectionFlagSet([]) + + expect(flags.of()).toEqual(set()) + expect(flags.of('a')).toEqual(set('a')) + expect(flags.of('x', 'y', 'z')).toEqual(set('x', 'y', 'z')) + }) + + test('named', () => { + const flags = createCollectionFlagSet([ + { value: 12, as: 'a' }, + { value: 45, as: 'b' }, + { value: 78, as: 'c' }, + { compose: ['a', 'b'], as: 'ab' }, + ]) + + expect(flags.named()).toEqual(set()) + expect(flags.named('a')).toEqual(set(12)) + expect(flags.named('ab', 'c')).toEqual(set(12, 45, 78)) + }) + test('union', () => { - const flags = new CollectionFlagSet() + const flags = createCollectionFlagSet([]) expect(flags.union(set(), set())).toEqual(set()) expect(flags.union(set('A'), set())).toEqual(set('A')) @@ -23,7 +45,7 @@ describe(CollectionFlagSet, () => { }) test('difference', () => { - const flags = new CollectionFlagSet() + const flags = createCollectionFlagSet([]) expect(flags.difference(set(), set())).toEqual(set()) expect(flags.difference(set('A'), set())).toEqual(set('A')) @@ -33,7 +55,7 @@ describe(CollectionFlagSet, () => { }) test('intersection', () => { - const flags = new CollectionFlagSet() + const flags = createCollectionFlagSet([]) expect(flags.intersection(set(), set())).toEqual(set()) expect(flags.intersection(set('A'), set())).toEqual(set()) @@ -44,7 +66,7 @@ describe(CollectionFlagSet, () => { }) test('isSuperset', () => { - const flags = new CollectionFlagSet() + const flags = createCollectionFlagSet([]) expect(flags.isSuperset(set(), set())).toBe(true) expect(flags.isSuperset(set('A', 'B'), set())).toBe(true) @@ -55,7 +77,7 @@ describe(CollectionFlagSet, () => { }) test('enumerate', () => { - const flags = new CollectionFlagSet() + const flags = createCollectionFlagSet([]) expect([...flags.enumerate(set())]).toEqual([]) expect([...flags.enumerate(set('A'))]).toEqual(['A']) diff --git a/node/tests/flagsets/number.test.ts b/node/tests/flagsets/number.test.ts index 2490eb4..156f780 100644 --- a/node/tests/flagsets/number.test.ts +++ b/node/tests/flagsets/number.test.ts @@ -34,37 +34,45 @@ describe(BitFlagSet, () => { }) test('union', () => { - expect(BitFlags.union(0, 0)).toEqual(0) - expect(BitFlags.union(1, 0)).toEqual(1) - expect(BitFlags.union(0, 2)).toEqual(2) - expect(BitFlags.union(1, 2)).toEqual(3) - expect(BitFlags.union(3, 6)).toEqual(7) + const flags = createBitFlagSet([]) + + expect(flags.union(0, 0)).toEqual(0) + expect(flags.union(1, 0)).toEqual(1) + expect(flags.union(0, 2)).toEqual(2) + expect(flags.union(1, 2)).toEqual(3) + expect(flags.union(3, 6)).toEqual(7) }) test('difference', () => { - expect(BitFlags.difference(0, 0)).toEqual(0) - expect(BitFlags.difference(1, 0)).toEqual(1) - expect(BitFlags.difference(3, 6)).toEqual(1) - expect(BitFlags.difference(6, 3)).toEqual(4) - expect(BitFlags.difference(8, 17)).toEqual(8) + const flags = createBitFlagSet([]) + + expect(flags.difference(0, 0)).toEqual(0) + expect(flags.difference(1, 0)).toEqual(1) + expect(flags.difference(3, 6)).toEqual(1) + expect(flags.difference(6, 3)).toEqual(4) + expect(flags.difference(8, 17)).toEqual(8) }) test('intersection', () => { - expect(BitFlags.intersection(0, 0)).toEqual(0) - expect(BitFlags.intersection(1, 0)).toEqual(0) - expect(BitFlags.intersection(1, 2)).toEqual(0) - expect(BitFlags.intersection(1, 3)).toEqual(1) - expect(BitFlags.intersection(11, 5)).toEqual(1) - expect(BitFlags.intersection(11, 7)).toEqual(3) + const flags = createBitFlagSet([]) + + expect(flags.intersection(0, 0)).toEqual(0) + expect(flags.intersection(1, 0)).toEqual(0) + expect(flags.intersection(1, 2)).toEqual(0) + expect(flags.intersection(1, 3)).toEqual(1) + expect(flags.intersection(11, 5)).toEqual(1) + expect(flags.intersection(11, 7)).toEqual(3) }) test('isSuperset', () => { - expect(BitFlags.isSuperset(0, 0)).toBe(true) - expect(BitFlags.isSuperset(3, 0)).toBe(true) - expect(BitFlags.isSuperset(3, 1)).toBe(true) - expect(BitFlags.isSuperset(3, 3)).toBe(true) - expect(BitFlags.isSuperset(0, 3)).toBe(false) - expect(BitFlags.isSuperset(8, 4)).toBe(false) + const flags = createBitFlagSet([]) + + expect(flags.isSuperset(0, 0)).toBe(true) + expect(flags.isSuperset(3, 0)).toBe(true) + expect(flags.isSuperset(3, 1)).toBe(true) + expect(flags.isSuperset(3, 3)).toBe(true) + expect(flags.isSuperset(0, 3)).toBe(false) + expect(flags.isSuperset(8, 4)).toBe(false) }) test('hasAny', () => { @@ -100,12 +108,14 @@ describe(BitFlagSet, () => { }) test('enumerate', () => { - expect([...BitFlags.enumerate(0)]).toEqual([]) - expect([...BitFlags.enumerate(1)]).toEqual([1]) - expect([...BitFlags.enumerate(2)]).toEqual([2]) - expect([...BitFlags.enumerate(3)]).toEqual([1, 2]) - expect([...BitFlags.enumerate(11)]).toEqual([1, 2, 8]) - expect([...BitFlags.enumerate(100)]).toEqual([4, 32, 64]) + const flags = createBitFlagSet([]) + + expect([...flags.enumerate(0)]).toEqual([]) + expect([...flags.enumerate(1)]).toEqual([1]) + expect([...flags.enumerate(2)]).toEqual([2]) + expect([...flags.enumerate(3)]).toEqual([1, 2]) + expect([...flags.enumerate(11)]).toEqual([1, 2, 8]) + expect([...flags.enumerate(100)]).toEqual([4, 32, 64]) }) test('maximum', () => { @@ -140,3 +150,48 @@ describe(BitFlagSet, () => { expect(flags.minimum(13)).toEqual(5) }) }) + +describe('BitFlags', () => { + test('union', () => { + expect(BitFlags.union(0, 0)).toEqual(0) + expect(BitFlags.union(1, 0)).toEqual(1) + expect(BitFlags.union(0, 2)).toEqual(2) + expect(BitFlags.union(1, 2)).toEqual(3) + expect(BitFlags.union(3, 6)).toEqual(7) + }) + + test('difference', () => { + expect(BitFlags.difference(0, 0)).toEqual(0) + expect(BitFlags.difference(1, 0)).toEqual(1) + expect(BitFlags.difference(3, 6)).toEqual(1) + expect(BitFlags.difference(6, 3)).toEqual(4) + expect(BitFlags.difference(8, 17)).toEqual(8) + }) + + test('intersection', () => { + expect(BitFlags.intersection(0, 0)).toEqual(0) + expect(BitFlags.intersection(1, 0)).toEqual(0) + expect(BitFlags.intersection(1, 2)).toEqual(0) + expect(BitFlags.intersection(1, 3)).toEqual(1) + expect(BitFlags.intersection(11, 5)).toEqual(1) + expect(BitFlags.intersection(11, 7)).toEqual(3) + }) + + test('isSuperset', () => { + expect(BitFlags.isSuperset(0, 0)).toBe(true) + expect(BitFlags.isSuperset(3, 0)).toBe(true) + expect(BitFlags.isSuperset(3, 1)).toBe(true) + expect(BitFlags.isSuperset(3, 3)).toBe(true) + expect(BitFlags.isSuperset(0, 3)).toBe(false) + expect(BitFlags.isSuperset(8, 4)).toBe(false) + }) + + test('enumerate', () => { + expect([...BitFlags.enumerate(0)]).toEqual([]) + expect([...BitFlags.enumerate(1)]).toEqual([1]) + expect([...BitFlags.enumerate(2)]).toEqual([2]) + expect([...BitFlags.enumerate(3)]).toEqual([1, 2]) + expect([...BitFlags.enumerate(11)]).toEqual([1, 2, 8]) + expect([...BitFlags.enumerate(100)]).toEqual([4, 32, 64]) + }) +})