implementation of minimum, maximum, hasAny and hasAll for bitflags

This commit is contained in:
2026-03-03 21:28:28 +01:00
parent c76b5c3f0a
commit 452810e6bf
22 changed files with 760 additions and 567 deletions

View File

@@ -1,47 +1,47 @@
{ {
"name": "multiflag", "name": "multiflag",
"version": "2.0.0", "version": "2.0.0",
"description": "flag/bitflag helper", "description": "flag/bitflag helper",
"main": "index.js", "main": "index.js",
"scripts": { "repository": {
"build": "tsup", "type": "git",
"postbuild": "cp package.json README.md ../LICENSE dist", "url": "git+https://github.com/louisdevie/multiflag.git"
"test": "vitest --ui", },
"coverage": "vitest run --coverage", "keywords": [
"type-check": "tsc --noEmit", "flag",
"format": "prettier --write src/ tests/" "bitflag"
},
"tsup": {
"entry": [
"src/index.ts"
], ],
"splitting": false, "author": "Louis DEVIE",
"dts": true, "license": "MIT",
"minify": true, "bugs": {
"sourcemap": true, "url": "https://github.com/louisdevie/tatsuki/issues"
"clean": true },
}, "homepage": "https://github.com/louisdevie/multiflag#readme",
"repository": { "scripts": {
"type": "git", "build": "tsup",
"url": "git+https://github.com/louisdevie/multiflag.git" "postbuild": "cp package.json README.md ../LICENSE dist",
}, "test": "vitest --ui",
"keywords": [ "coverage": "vitest run --coverage",
"flag", "type-check": "tsc --noEmit",
"bitflag" "format": "prettier --write src/ tests/"
], },
"author": "Louis DEVIE", "tsup": {
"license": "MIT", "entry": [
"bugs": { "src/index.ts"
"url": "https://github.com/louisdevie/multiflag/issues" ],
}, "splitting": false,
"homepage": "https://github.com/louisdevie/multiflag#readme", "dts": true,
"devDependencies": { "minify": true,
"@vitest/coverage-v8": "^4.0.18", "sourcemap": true,
"@vitest/ui": "^4.0.18", "clean": true
"prettier": "^3.8.1", },
"ts-node": "^10.9.2", "devDependencies": {
"tsup": "^8.5.1", "@vitest/coverage-v8": "^4.0.18",
"typescript": "^5.9.3", "@vitest/ui": "^4.0.18",
"vitest": "^4.0.18" "prettier": "^3.8.1",
} "ts-node": "^10.9.2",
"tsup": "^8.5.1",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
}
} }

View File

@@ -1,8 +1,5 @@
import type { import type { DefineWithOrdinal, DefineWithValue, DefineWithValueOrOrdinal } from './syntax'
DefineWithOrdinal, import { FlagsGraph, refByAlias } from '~/definitions'
DefineWithValue,
DefineWithValueOrOrdinal,
} from './syntax'
// Generic helper functions // Generic helper functions
@@ -10,9 +7,7 @@ import type {
* Copies the record keys into the 'as' property of the values and return an * Copies the record keys into the 'as' property of the values and return an
* array containing those values. * array containing those values.
*/ */
function toDefinitionArray<D>( function toDeclarationArray<D>(record: Record<string, D>): (D & { as: string })[] {
record: Record<string, D>,
): (D & { as: string })[] {
return Object.keys(record).map((key) => ({ ...record[key], as: key })) return Object.keys(record).map((key) => ({ ...record[key], as: key }))
} }
@@ -22,35 +17,9 @@ export type FlagWithValue<F> =
| { value: F; as?: string; requires?: string[] } | { value: F; as?: string; requires?: string[] }
| { compose: string[]; as: string } | { compose: string[]; as: string }
export type NamedFlagWithValue<F> = export type NamedFlagWithValue<F> = { value: F; requires?: string[] } | { compose: string[] }
| { value: F; requires?: string[] }
| { compose: string[] }
export type ListOfFlagsWithValue<F> = export type ListOfFlagsWithValue<F> = FlagWithValue<F>[] | Record<string, NamedFlagWithValue<F>>
| FlagWithValue<F>[]
| Record<string, NamedFlagWithValue<F>>
export function applyDeclarationsWithValue<F>(
declarations: ListOfFlagsWithValue<F>,
builder: DefineWithValue<F, unknown, unknown>,
) {
const declarationsArray = Array.isArray(declarations)
? declarations
: toDefinitionArray(declarations)
for (const declaration of declarationsArray) {
if ('compose' in declaration) {
builder.define(declaration.as).compose(...declaration.compose)
} else {
// it doesn't matter if "as" is undefined,
// we can always call withValue() on the returned value
builder
.define(declaration.as!)
.withValue(declaration.value)
.requires(...(declaration.requires ?? []))
}
}
}
// Declarations for builders that supports only definitions by ordinal // Declarations for builders that supports only definitions by ordinal
@@ -66,28 +35,6 @@ export type ListOfFlagsWithOrdinal<F> =
| FlagWithOrdinal<F>[] | FlagWithOrdinal<F>[]
| Record<string, NamedFlagWithOrdinal<F>> | Record<string, NamedFlagWithOrdinal<F>>
export function applyDeclarationsWithOrdinal<F>(
declarations: ListOfFlagsWithOrdinal<F>,
builder: DefineWithOrdinal<F, unknown, unknown>,
) {
const declarationsArray = Array.isArray(declarations)
? declarations
: toDefinitionArray(declarations)
for (const declaration of declarationsArray) {
if ('compose' in declaration) {
builder.define(declaration.as).compose(...declaration.compose)
} else {
// it doesn't matter if "as" is undefined,
// we can always call withOrdinal() on the returned value
builder
.define(declaration.as!)
.withOrdinal(declaration.ordinal)
.requires(...(declaration.requires ?? []))
}
}
}
// Declarations for builders that supports definitions by value and ordinal // Declarations for builders that supports definitions by value and ordinal
export type FlagWithValueOrOrdinal<F> = export type FlagWithValueOrOrdinal<F> =
@@ -104,13 +51,13 @@ export type ListOfFlagsWithValueOrOrdinal<F> =
| FlagWithValueOrOrdinal<F>[] | FlagWithValueOrOrdinal<F>[]
| Record<string, NamedFlagWithValueOrOrdinal<F>> | Record<string, NamedFlagWithValueOrOrdinal<F>>
export function applyDeclarationsWithValueOrOrdinal<F>( export function applyDeclarations<F>(
declarations: ListOfFlagsWithValueOrOrdinal<F>, declarations: ListOfFlagsWithValueOrOrdinal<F>,
builder: DefineWithValueOrOrdinal<F, unknown, unknown>, builder: DefineWithValueOrOrdinal<F, unknown, unknown>,
) { ) {
const declarationsArray = Array.isArray(declarations) const declarationsArray = Array.isArray(declarations)
? declarations ? declarations
: toDefinitionArray(declarations) : toDeclarationArray(declarations)
for (const declaration of declarationsArray) { for (const declaration of declarationsArray) {
if ('compose' in declaration) { if ('compose' in declaration) {

View File

@@ -1,94 +1,43 @@
import { InvalidOperationError, InvalidReferenceError } from '../errors' import { InvalidOperationError } from '~/errors'
import { FlagDefinition, FlagsDictionary } from '~' import { FlagsGraph, PartialFlagDefinition, PartialFlagInit, refByAlias } from '~/definitions'
import { printFlagValue } from '../definitions'
import { GenericFlagsDictionary } from '../definitions/dictionary'
export interface FlagDefinitionFactory<F, S> { export class FlagSetBuilder<F> {
readonly supportsDefinitionsByValue: boolean private readonly _graph: FlagsGraph<F>
readonly supportsDefinitionsByOrdinal: boolean private _currentDefinition: PartialFlagInit<F> | undefined
precomputeTopDown(partialDefinition: PartialDefinition<F, S>): void public constructor() {
this._graph = new FlagsGraph()
precomputeBottomUp(partialDefinition: PartialDefinition<F, S>): void
createFlagDefinition(partialDefinition: PartialDefinition<F, S>): FlagDefinition<F, S>
}
export interface PartialDefinition<F, S> {
alias?: string
value?: F
parentRefs?: Set<string>
parents?: Set<PartialDefinition<F, S>>
children?: Set<PartialDefinition<F, S>>
baseValues?: S
additiveValues?: S
subtractiveValues?: S
}
function addRelationship<F, S>(
parent: PartialDefinition<F, S>,
child: PartialDefinition<F, S>,
): void {
if (parent.children === undefined) {
parent.children = new Set([child])
} else {
parent.children.add(child)
}
if (child.parents === undefined) {
child.parents = new Set([parent])
} else {
child.parents.add(parent)
}
}
export class GenericFlagSetBuilder<F, S> {
private readonly _factory: FlagDefinitionFactory<F, S>
private readonly _definitions: Set<PartialDefinition<F, S>>
private _currentDefinition: PartialDefinition<F, S> | undefined
public constructor(factory: FlagDefinitionFactory<F, S>) {
this._factory = factory
this._definitions = new Set()
this._currentDefinition = undefined this._currentDefinition = undefined
} }
private get canMoveOnToNextDefinition(): boolean {
return (
this._currentDefinition === undefined ||
this._currentDefinition.value !== undefined ||
this._currentDefinition.parentRefs !== undefined
)
}
private createNewDefinition(alias: string | undefined): void {
const def = { alias }
this._definitions.add(def)
this._currentDefinition = def
}
public define(alias: string | undefined): void { public define(alias: string | undefined): void {
if (!this.canMoveOnToNextDefinition) { if (
this._currentDefinition !== undefined &&
this._currentDefinition.value === undefined &&
this._currentDefinition.parents === undefined
) {
throw new InvalidOperationError('define') throw new InvalidOperationError('define')
} }
this.createNewDefinition(alias) this.finishCurrentDefinition()
this._currentDefinition = { alias }
} }
public compose(flags: string[]): void { public compose(flags: string[]): void {
if ( if (
this._currentDefinition === undefined || this._currentDefinition === undefined ||
this._currentDefinition.alias === undefined || this._currentDefinition.alias === undefined ||
this._currentDefinition.parentRefs !== undefined this._currentDefinition.parents !== undefined
) { ) {
throw new InvalidOperationError('compose') throw new InvalidOperationError('compose')
} }
this._currentDefinition.parentRefs = new Set(flags) this._currentDefinition.parents = refByAlias([...new Set(flags)])
} }
public withValue(value: F): void { public withValue(value: F): void {
if ( if (
this._currentDefinition === undefined || this._currentDefinition === undefined ||
this._currentDefinition.value !== undefined || this._currentDefinition.value !== undefined ||
this._currentDefinition.parentRefs !== undefined this._currentDefinition.parents !== undefined
) { ) {
throw new InvalidOperationError('withValue') throw new InvalidOperationError('withValue')
} }
@@ -99,14 +48,25 @@ export class GenericFlagSetBuilder<F, S> {
if ( if (
this._currentDefinition === undefined || this._currentDefinition === undefined ||
this._currentDefinition.value === undefined || this._currentDefinition.value === undefined ||
this._currentDefinition.parentRefs !== undefined this._currentDefinition.parents !== undefined
) { ) {
throw new InvalidOperationError('requires') throw new InvalidOperationError('requires')
} }
this._currentDefinition.parentRefs = new Set(flags) this._currentDefinition.parents = refByAlias([...new Set(flags)])
} }
public buildDictionary(): FlagsDictionary<F, S> { private finishCurrentDefinition() {
if (this._currentDefinition !== undefined) {
this._graph.put(this._currentDefinition)
}
this._currentDefinition = undefined
}
public finish(): FlagsGraph<F> {
this.finishCurrentDefinition()
return this._graph
}
/*
// this array will contain the nodes of the graph, in topological order // this array will contain the nodes of the graph, in topological order
const sorted: PartialDefinition<F, S>[] = [] const sorted: PartialDefinition<F, S>[] = []
@@ -157,15 +117,14 @@ export class GenericFlagSetBuilder<F, S> {
this._factory.precomputeBottomUp(sorted[i]) this._factory.precomputeBottomUp(sorted[i])
dict.add(this._factory.createFlagDefinition(sorted[i])) dict.add(this._factory.createFlagDefinition(sorted[i]))
} }
return dict return dict*/
}
private displayPartialDefinition(definition: PartialDefinition<F, S>): string { /*private displayPartialDefinition(definition: PartialDefinition<F, S>): string {
if (definition.alias) { if (definition.alias) {
return '"' + definition.alias + '"' return '"' + definition.alias + '"'
} else { } else {
// generate the definition and try to print its base value // generate the definition and try to print its base value
return printFlagValue(this._factory.createFlagDefinition(definition)) return printFlagValue(this._factory.createFlagDefinition(definition))
} }
} }*/
} }

View File

@@ -1,4 +1,3 @@
import { BitFlagSet, FlagDefinition, FlagsDictionary } from '~'
import { import {
DefineWithValueOrOrdinal, DefineWithValueOrOrdinal,
RequireParentsOrDefineWithValueOrOrdinal, RequireParentsOrDefineWithValueOrOrdinal,
@@ -6,23 +5,24 @@ import {
WithValueOrOrdinalOrCompose, WithValueOrOrdinalOrCompose,
} from './syntax' } from './syntax'
import { import {
applyDeclarationsWithValueOrOrdinal, applyDeclarations,
FlagWithValueOrOrdinal, FlagWithValueOrOrdinal,
ListOfFlagsWithValueOrOrdinal, ListOfFlagsWithValueOrOrdinal,
NamedFlagWithValueOrOrdinal, NamedFlagWithValueOrOrdinal,
} from './declarative' } from './declarative'
import { BitFlagDefinition } from '../definitions' import { BitFlagDefinitionFactory, FlagDefinition } from '~/definitions'
import { FlagDefinitionFactory, GenericFlagSetBuilder, PartialDefinition } from './generic' import { FlagSetBuilder } from './generic'
import { BitFlagSet } from '~/flagsets'
export class BitFlagSetBuilder export class BitFlagSetBuilder
implements implements
WithValueOrOrdinalOrCompose<number, number, BitFlagSet>, WithValueOrOrdinalOrCompose<number, number, BitFlagSet>,
RequireParentsOrDefineWithValueOrOrdinal<number, number, BitFlagSet> RequireParentsOrDefineWithValueOrOrdinal<number, number, BitFlagSet>
{ {
private readonly _underlying: GenericFlagSetBuilder<number, number> private readonly _underlying: FlagSetBuilder<number>
public constructor() { public constructor() {
this._underlying = new GenericFlagSetBuilder(new BitFlagDefinitionFactory()) this._underlying = new FlagSetBuilder()
} }
public define(): WithValueOrOrdinal<number, number, BitFlagSet> public define(): WithValueOrOrdinal<number, number, BitFlagSet>
@@ -55,60 +55,11 @@ export class BitFlagSetBuilder
return this return this
} }
public getDictionary(): FlagsDictionary<number, number> {
return this._underlying.buildDictionary() as any
}
public getResult(): BitFlagSet { public getResult(): BitFlagSet {
return new BitFlagSet(this.getDictionary()) const graph = this._underlying.finish()
} const definitions = graph.sortedDefinitions()
} const factory = new BitFlagDefinitionFactory()
return new BitFlagSet(graph.intoDictionary(factory))
class BitFlagDefinitionFactory implements FlagDefinitionFactory<number, number> {
public readonly supportsDefinitionsByOrdinal = true
public readonly supportsDefinitionsByValue = true
public precomputeTopDown(partialDefinition: PartialDefinition<number, number>): void {
partialDefinition.additiveValues = 0
partialDefinition.baseValues = 0
if (partialDefinition.value !== undefined) {
partialDefinition.additiveValues = partialDefinition.value
partialDefinition.baseValues = partialDefinition.value
}
if (partialDefinition.parents !== undefined) {
for (const parent of partialDefinition.parents.values()) {
partialDefinition.additiveValues =
partialDefinition.additiveValues | (parent.additiveValues ?? 0)
if (partialDefinition.value === undefined) {
partialDefinition.baseValues =
partialDefinition.baseValues | (parent.baseValues ?? 0)
}
}
}
}
public precomputeBottomUp(partialDefinition: PartialDefinition<number, number>): void {
partialDefinition.subtractiveValues = 0
if (partialDefinition.value) {
partialDefinition.subtractiveValues = partialDefinition.value
}
if (partialDefinition.children) {
for (const child of partialDefinition.children.values()) {
partialDefinition.subtractiveValues =
partialDefinition.subtractiveValues | (child.subtractiveValues ?? 0)
}
}
}
public createFlagDefinition(
partialDefinition: PartialDefinition<number, number>,
): FlagDefinition<number, number> {
return new BitFlagDefinition(
partialDefinition.baseValues ?? 0,
partialDefinition.additiveValues ?? 0,
partialDefinition.subtractiveValues ?? 0,
partialDefinition.alias,
)
} }
} }
@@ -118,6 +69,6 @@ export function createBitFlagSet<D extends string>(
): BitFlagSet & Record<D, FlagDefinition<number, number>> ): BitFlagSet & Record<D, FlagDefinition<number, number>>
export function createBitFlagSet(declarations: ListOfFlagsWithValueOrOrdinal<number>): BitFlagSet { export function createBitFlagSet(declarations: ListOfFlagsWithValueOrOrdinal<number>): BitFlagSet {
const builder = new BitFlagSetBuilder() const builder = new BitFlagSetBuilder()
applyDeclarationsWithValueOrOrdinal(declarations, builder) applyDeclarations(declarations, builder)
return builder.getResult() return builder.getResult()
} }

View File

@@ -1,11 +1,6 @@
import type { FlagsDictionary, FlagSet } from '~' import type { FlagsDictionary, FlagSet } from '~'
export interface Root<F, S, R = FlagSet<F, S>> { export interface Root<F, S, R = FlagSet<F, S>> {
/**
* Build a dictionary with the flags previously defined.
*/
getDictionary(): FlagsDictionary<F, S>
/** /**
* Build a flag set with the flags previously defined. * Build a flag set with the flags previously defined.
*/ */
@@ -14,8 +9,7 @@ export interface Root<F, S, R = FlagSet<F, S>> {
// Syntax for builders that supports only definitions by value // Syntax for builders that supports only definitions by value
export interface DefineWithValue<F, S, R = FlagSet<F, S>> export interface DefineWithValue<F, S, R = FlagSet<F, S>> extends Root<F, S, R> {
extends Root<F, S, R> {
/** /**
* Define an anonymous flag. * Define an anonymous flag.
*/ */
@@ -44,8 +38,7 @@ export interface WithValueOrCompose<F, S, R> extends WithValue<F, S, R> {
compose(...flags: string[]): DefineWithValue<F, S, R> compose(...flags: string[]): DefineWithValue<F, S, R>
} }
export interface RequireParentsOrDefineWithValue<F, S, R> export interface RequireParentsOrDefineWithValue<F, S, R> extends DefineWithValue<F, S, R> {
extends DefineWithValue<F, S, R> {
/** /**
* Set the parents of this flag. * Set the parents of this flag.
* @param flags The names of the parent flags. * @param flags The names of the parent flags.
@@ -55,8 +48,7 @@ export interface RequireParentsOrDefineWithValue<F, S, R>
// Syntax for builders that supports only definitions by ordinal // Syntax for builders that supports only definitions by ordinal
export interface DefineWithOrdinal<F, S, R = FlagSet<F, S>> export interface DefineWithOrdinal<F, S, R = FlagSet<F, S>> extends Root<F, S, R> {
extends Root<F, S, R> {
/** /**
* Define an anonymous flag. * Define an anonymous flag.
*/ */
@@ -86,8 +78,7 @@ export interface WithOrdinalOrCompose<F, S, R> extends WithOrdinal<F, S, R> {
compose(...flags: string[]): DefineWithOrdinal<F, S, R> compose(...flags: string[]): DefineWithOrdinal<F, S, R>
} }
export interface RequireParentsOrDefineWithOrdinal<F, S, R> export interface RequireParentsOrDefineWithOrdinal<F, S, R> extends DefineWithOrdinal<F, S, R> {
extends DefineWithOrdinal<F, S, R> {
/** /**
* Set the parents of this flag. * Set the parents of this flag.
* @param flags The names of the parent flags. * @param flags The names of the parent flags.
@@ -97,8 +88,7 @@ export interface RequireParentsOrDefineWithOrdinal<F, S, R>
// Syntax for builders that supports definitions by value and ordinal // Syntax for builders that supports definitions by value and ordinal
export interface DefineWithValueOrOrdinal<F, S, R = FlagSet<F, S>> export interface DefineWithValueOrOrdinal<F, S, R = FlagSet<F, S>> extends Root<F, S, R> {
extends Root<F, S, R> {
/** /**
* Define an anonymous flag. * Define an anonymous flag.
*/ */
@@ -123,13 +113,10 @@ export interface WithValueOrOrdinal<F, S, R> {
* @param ordinal The number of the flag (starting at 1). A unique value * @param ordinal The number of the flag (starting at 1). A unique value
* will be assigned based on this number. * will be assigned based on this number.
*/ */
withOrdinal( withOrdinal(ordinal: number): RequireParentsOrDefineWithValueOrOrdinal<F, S, R>
ordinal: number,
): RequireParentsOrDefineWithValueOrOrdinal<F, S, R>
} }
export interface WithValueOrOrdinalOrCompose<F, S, R> export interface WithValueOrOrdinalOrCompose<F, S, R> extends WithValueOrOrdinal<F, S, R> {
extends WithValueOrOrdinal<F, S, R> {
/** /**
* Define this flag as a composed flag. * Define this flag as a composed flag.
* @param flags The name of the flags in the group. * @param flags The name of the flags in the group.
@@ -137,8 +124,11 @@ export interface WithValueOrOrdinalOrCompose<F, S, R>
compose(...flags: string[]): DefineWithValueOrOrdinal<F, S, R> compose(...flags: string[]): DefineWithValueOrOrdinal<F, S, R>
} }
export interface RequireParentsOrDefineWithValueOrOrdinal<F, S, R> export interface RequireParentsOrDefineWithValueOrOrdinal<F, S, R> extends DefineWithValueOrOrdinal<
extends DefineWithValueOrOrdinal<F, S, R> { F,
S,
R
> {
/** /**
* Set the parents of this flag. * Set the parents of this flag.
* @param flags The names of the parent flags. * @param flags The names of the parent flags.

View File

@@ -1,67 +1,48 @@
import type { FlagDefinition } from '.'
import { ReusedFlagAliasError, ReusedFlagValueError } from '../errors'
/**
* A collection of {@link FlagDefinition}s.
*/
export interface FlagsDictionary<F, S> { export interface FlagsDictionary<F, S> {
/**
* Search for a flag in the collection.
* @param alias The alias of the flag.
* @returns The corresponding definition, or `undefined` if there is no flag
* with this alias.
*/
findByAlias(alias: string): FlagDefinition<F, S> | undefined findByAlias(alias: string): FlagDefinition<F, S> | undefined
findByValue(value: F): FlagDefinition<F, S> | undefined
} }
/** export interface FlagDefinition<F, S> {
* Built-in dictionary implementation. /**
* @internal * The alias of the flag.
*/ */
export class GenericFlagsDictionary<F, S> implements FlagsDictionary<F, S> { readonly alias: string | undefined
private readonly _named: Map<string, FlagDefinition<F, S>>
private readonly _anonymous: FlagDefinition<F, S>[]
public constructor() {
this._named = new Map()
this._anonymous = []
}
public add(definition: FlagDefinition<F, S>): void {
if (definition.alias === undefined) {
this._anonymous.push(definition)
} else {
this._named.set(definition.alias, definition)
}
}
/** /**
* Search for a flag in the collection. * A set containing the value of the flag, or multiple values if it is a composed flag.
* @param alias The alias of the flag.
* @returns The corresponding definition, or `undefined` if there is no flag
* with this alias.
*/ */
public findByAlias(alias: string): FlagDefinition<F, S> | undefined { readonly values: S
return this._named.get(alias)
} /**
* Test if this flag and all its parents are present in the set.
* @param set A set of flags.
* @returns `true` if the set includes this flags and its parents.
*/
isIn(set: S): boolean
/**
* Add this flag and all its parents to the set.
* @param set A set of flags.
* @returns A new set of flags containing the flags from the `set`, this flag and its parents.
*/
addTo(set: S): S
/**
* Removes this flag and all its children from the set.
* @param set A set of flags.
* @returns A new set of flags containing the flags from the `set` except this flag and its children.
*/
removeFrom(set: S): S
} }
/* export const valueToString = Symbol()
public define(definition: FlagDefinition<F, S>) {
for (const other of this._named.values()) { export function printFlagValue(flag: FlagDefinition<unknown, unknown>): string {
if ( if (valueToString in flag) {
definition.alias !== undefined && return (flag[valueToString] as Function)()
definition.alias === other.alias } else {
) { return String(flag.values)
throw new ReusedFlagAliasError(definition.alias) }
} }
if (definition.hasSameValue(other)) {
throw new ReusedFlagValueError(definition, other)
}
}
for (const other of this._anonymous) {
if (definition.hasSameValue(other)) {
throw new ReusedFlagValueError(definition, other)
}
}
*/

View File

@@ -0,0 +1,258 @@
import { FlagDefinition, FlagsDictionary, printFlagValue } from '~/definitions'
import { InternalError } from '~/errors'
export interface PartialFlagInit<F> {
alias?: string
value?: F
parents?: PartialFlagInit<F>[]
}
export function refByAlias(refs: string[]): PartialFlagInit<never>[] {
return refs.map((ref) => ({ alias: ref }))
}
export interface FlagDefinitionFactory<F, S> {
makeDefinitions(
sortedPartialDefinitions: PartialFlagDefinition<F>[],
results: Map<PartialFlagDefinition<F>, FlagDefinition<F, S>>,
): void
}
/**
* A graph with {@link PartialFlagDefinition} vertices.
*/
export class FlagsGraph<F> {
private readonly _definitions: PartialFlagDefinition<F>[]
private readonly _aliasToDefinition: Map<string, PartialFlagDefinition<F>>
private readonly _valueToDefinition: Map<F, PartialFlagDefinition<F>>
public constructor() {
this._definitions = []
this._aliasToDefinition = new Map()
this._valueToDefinition = new Map()
}
private add(pfd: PartialFlagDefinition<F>): void {
this._definitions.push(pfd)
if (pfd.alias !== undefined) {
this._aliasToDefinition.set(pfd.alias, pfd)
}
if (pfd.value !== undefined) {
this._valueToDefinition.set(pfd.value, pfd)
}
}
/**
* Search for a flag matching the alias or value of `init`. If no flag is found, a new
* placeholder flag is created to make a forward reference.
*/
private findOrAddPlaceholder(init: PartialFlagInit<F>): PartialFlagDefinition<F> {
let pfd
if (init.alias !== undefined) {
pfd = this._aliasToDefinition.get(init.alias)
}
if (pfd === undefined) {
pfd = new PartialFlagDefinition(true, init)
this.add(pfd)
}
return pfd
}
/**
* Add a new flag to the graph, or upgrade the existing definition.
*/
public put(init: PartialFlagInit<F>): PartialFlagDefinition<F> {
let pfd
if (init.alias !== undefined) {
pfd = this._aliasToDefinition.get(init.alias)
}
if (pfd === undefined) {
pfd = new PartialFlagDefinition(false, init)
this.add(pfd)
} else {
pfd.upgrade(init)
}
if (init.parents !== undefined) {
for (const parentInit of init.parents) {
const parentPfd = this.findOrAddPlaceholder(parentInit)
pfd.addParent(parentPfd)
}
}
return pfd
}
/**
* A list of all definitions that are not placeholders.
*/
public get definitions(): Iterable<PartialFlagDefinition<F>> {
return this._definitions.filter((d) => !d.isPlaceholder)
}
/**
* Return an array containing the definitions in topological order.
* @throws CircularReferenceError when the graph can not be sorted because it contains a cycle.
*/
private sortedDefinitions(): PartialFlagDefinition<F>[] {
// work on a copy of the graph
const remaining = new Map(this._definitions.map((pfd) => [pfd, new Set(pfd.parents)]))
const sorted: PartialFlagDefinition<F>[] = []
const start: PartialFlagDefinition<F>[] = []
for (const pfd of [...remaining.keys()]) {
const parents = remaining.get(pfd)!
if (parents.size === 0) {
start.push(pfd)
remaining.delete(pfd)
}
}
while (start.length > 0) {
const current = start.pop()!
sorted.push(current)
for (const child of current.children) {
const parents = remaining.get(child)!
parents.delete(current)
if (parents.size === 0) {
start.push(child)
remaining.delete(child)
}
}
}
return sorted
}
public intoDictionary<S>(factory: FlagDefinitionFactory<F, S>): FlagsDictionary<F, S> {
const sortedPartialDefinitions = this.sortedDefinitions()
const definitions = new Map<PartialFlagDefinition<F>, FlagDefinition<F, S>>()
factory.makeDefinitions(sortedPartialDefinitions, definitions)
const aliasToDefinition = new Map<string, FlagDefinition<F, S>>()
for (const [alias, pfd] of this._aliasToDefinition.entries()) {
const definition = definitions.get(pfd)
if (definition === undefined) {
throw new InternalError(`factory didn't provide any definition for ${pfd}`)
}
aliasToDefinition.set(alias, definition)
}
const valueToDefinition = new Map<F, FlagDefinition<F, S>>()
for (const [value, pfd] of this._valueToDefinition.entries()) {
const definition = definitions.get(pfd)
if (definition === undefined) {
throw new InternalError(`factory didn't provide any definition for ${pfd}`)
}
valueToDefinition.set(value, definition)
}
return new BiMapFlagsDictionary(aliasToDefinition, valueToDefinition)
}
}
class BiMapFlagsDictionary<F, S> implements FlagsDictionary<F, S> {
private readonly _aliasToDefinition: Map<string, FlagDefinition<F, S>>
private readonly _valueToDefinition: Map<F, FlagDefinition<F, S>>
public constructor(
aliasToDefinition: Map<string, FlagDefinition<F, S>>,
valueToDefinition: Map<F, FlagDefinition<F, S>>,
) {
this._aliasToDefinition = aliasToDefinition
this._valueToDefinition = valueToDefinition
}
public findByAlias(alias: string): FlagDefinition<F, S> | undefined {
return this._aliasToDefinition.get(alias)
}
public findByValue(value: F): FlagDefinition<F, S> | undefined {
return this._valueToDefinition.get(value)
}
}
/**
* A partial {@link FlagDefinition} used for building a flag graph.
*/
export class PartialFlagDefinition<F> {
private readonly _alias: string | undefined
private readonly _parents: Set<PartialFlagDefinition<F>>
private readonly _children: Set<PartialFlagDefinition<F>>
private _isPlaceholder: boolean
private _value: F | undefined
public constructor(isPlaceholder: boolean, init: PartialFlagInit<F>) {
this._isPlaceholder = isPlaceholder
this._alias = init.alias
this._value = init.value
this._parents = new Set()
this._children = new Set()
}
/**
* The alias of the flag, or `undefined` if no alias has been set yet.
*/
public get alias(): string | undefined {
return this._alias
}
/**
* The value of the flag, or `undefined` if no value has been set yet.
*/
public get value(): F | undefined {
return this._value
}
/**
* Indicates that this flag has not been defined yet, but has been referenced by another flag.
*/
public get isPlaceholder(): boolean {
return this._isPlaceholder
}
/**
* Upgrade a placeholder flag to an actual definition.
*/
public upgrade(init: PartialFlagInit<F>): void {
this._value = init.value
this._isPlaceholder = false
}
/**
* The list of all parent flags (including forward references).
*/
public get parents(): Iterable<PartialFlagDefinition<F>> {
return this._parents
}
/**
* The list of all children flags.
*/
public get children(): Iterable<PartialFlagDefinition<F>> {
return this._children
}
/**
* Creates a parent-child relationship between this flag and another.
*/
public addParent(parentPfd: PartialFlagDefinition<F>): void {
this._parents.add(parentPfd)
parentPfd._children.add(this)
}
public toString(): string {
if (this._alias !== undefined) {
return '"' + this._alias + '"'
} else if (this._value !== undefined) {
return JSON.stringify(this._value)
} else {
return '[?]'
}
}
}

View File

@@ -1,23 +1,9 @@
export { FlagsDictionary } from './dictionary' export { FlagsDictionary, FlagDefinition, printFlagValue, valueToString } from './dictionary'
export { BitFlagDefinition } from './number' export {
FlagsGraph,
export interface FlagDefinition<F, S> { PartialFlagInit,
/** PartialFlagDefinition,
* The alias of the flag. refByAlias,
*/ FlagDefinitionFactory,
readonly alias: string | undefined } from './graph'
export { BitFlagDefinition, BitFlagDefinitionFactory } from './number'
readonly values: S
hasSameValue(other: FlagDefinition<F, S>): boolean
}
export const valueToString = Symbol()
export function printFlagValue(flag: FlagDefinition<unknown, unknown>): string {
if (valueToString in flag) {
return (flag[valueToString] as Function)()
} else {
return String(flag.values)
}
}

View File

@@ -1,20 +1,21 @@
import { FlagDefinition, valueToString } from '.' import { FlagDefinition, FlagDefinitionFactory, PartialFlagDefinition } from '.'
interface PrecomputedValues {
base: number
additive: number
subtractive: number
}
export class BitFlagDefinition implements FlagDefinition<number, number> { export class BitFlagDefinition implements FlagDefinition<number, number> {
private readonly _baseValues: number private readonly _baseValue: number
private readonly _additiveValues: number private readonly _additiveValue: number
private readonly _subtractiveValues: number private readonly _subtractiveValue: number
private readonly _alias: string | undefined private readonly _alias: string | undefined
public constructor( public constructor(precomputedValues: PrecomputedValues, alias: string | undefined) {
baseValues: number, this._baseValue = precomputedValues.base
additiveValues: number, this._additiveValue = precomputedValues.additive
subtractiveValues: number, this._subtractiveValue = precomputedValues.subtractive
alias: string | undefined,
) {
this._baseValues = baseValues
this._additiveValues = additiveValues
this._subtractiveValues = subtractiveValues
this._alias = alias this._alias = alias
} }
@@ -23,10 +24,63 @@ export class BitFlagDefinition implements FlagDefinition<number, number> {
} }
public get values(): number { public get values(): number {
return this._baseValues return this._baseValue
} }
public hasSameValue(other: FlagDefinition<number, number>): boolean { public isIn(set: number): boolean {
return other instanceof BitFlagDefinition && this._baseValues === other._baseValues return (set & this._additiveValue) === this._additiveValue
}
public addTo(set: number): number {
return set | this._additiveValue
}
public removeFrom(set: number): number {
return set | this._additiveValue
}
}
export class BitFlagDefinitionFactory implements FlagDefinitionFactory<number, number> {
public makeDefinitions(
sortedPartialDefinitions: PartialFlagDefinition<number>[],
results: Map<PartialFlagDefinition<number>, FlagDefinition<number, number>>,
): void {
const precomputedValues = new Map<PartialFlagDefinition<number>, PrecomputedValues>()
for (let i = 0; i < sortedPartialDefinitions.length; i++) {
const pfd = sortedPartialDefinitions[i]
const values: PrecomputedValues = {
base: pfd.value ?? 0,
additive: pfd.value ?? 0,
subtractive: pfd.value ?? 0,
}
for (const parentPfd of pfd.parents) {
const parentValues = precomputedValues.get(parentPfd)
if (parentValues !== undefined) {
values.additive = values.additive | parentValues.additive
if (pfd.value === undefined) {
values.base = values.base | parentValues.base
}
}
}
precomputedValues.set(pfd, values)
}
for (let i = sortedPartialDefinitions.length - 1; i >= 0; i--) {
const pfd = sortedPartialDefinitions[i]
const values = precomputedValues.get(pfd)!
for (const childPfd of pfd.children) {
const childValues = precomputedValues.get(childPfd)
if (childValues !== undefined) {
values.subtractive = values.subtractive | childValues.subtractive
}
}
results.set(pfd, new BitFlagDefinition(values, pfd.alias))
}
} }
} }

View File

@@ -1,4 +1,5 @@
import { FlagDefinition, printFlagValue } from './definitions' import { FlagDefinition, printFlagValue } from './definitions'
import pkg from '../package.json'
/** /**
* Error thrown when a feature is not available in the current environment. * Error thrown when a feature is not available in the current environment.
@@ -38,10 +39,7 @@ export class ReusedFlagAliasError extends Error {
*/ */
export class ReusedFlagValueError extends Error { export class ReusedFlagValueError extends Error {
/** @internal */ /** @internal */
public constructor( public constructor(a: FlagDefinition<unknown, unknown>, b: FlagDefinition<unknown, unknown>) {
a: FlagDefinition<unknown, unknown>,
b: FlagDefinition<unknown, unknown>,
) {
super( super(
`The value ${printFlagValue(a)} is already being used ${b.alias ? 'for the flag "' + b.alias + '"' : 'by another flag'}.`, `The value ${printFlagValue(a)} is already being used ${b.alias ? 'for the flag "' + b.alias + '"' : 'by another flag'}.`,
) )
@@ -54,18 +52,33 @@ export class ReusedFlagValueError extends Error {
export class InvalidOperationError extends Error { export class InvalidOperationError extends Error {
/** @internal */ /** @internal */
public constructor(methodName: string, explanation?: string) { public constructor(methodName: string, explanation?: string) {
super( super(`.${methodName}() is not allowed here${explanation ? ': ' + explanation : ''}.`)
`.${methodName}() is not allowed here${explanation ? ': ' + explanation : ''}.`,
)
} }
} }
/** /**
* Error thrown by builders when referencing a flag that is not defined. * Error thrown by builders when a flag references a flag that is not defined.
*/ */
export class InvalidReferenceError extends Error { export class UndefinedReferenceError extends Error {
/** @internal */ /** @internal */
public constructor(alias: string, requiredBy: string) { public constructor(alias: string, requiredBy: string) {
super(`Undefined flag "${alias}" required by ${requiredBy}.`) super(`Undefined flag "${alias}" required by ${requiredBy}.`)
} }
} }
/**
* Error thrown by builders when a circular dependency is created.
*/
export class CircularReferenceError extends Error {
/** @internal */
public constructor(chain: string[]) {
super(`Circular reference found: ${chain.join(' → ')}.`)
}
}
export class InternalError extends Error {
/** @internal */
public constructor(message: string) {
super(`[Tatsuki internal error] ${message}. Please report it at ${pkg.bugs.url}.`)
}
}

View File

@@ -1,15 +1,6 @@
import type { FlagSet } from '.' import type { FlagSet } from '.'
import { import { decodeB64Byte, encodeB64Byte, normaliseB64String, ZERO_STRING } from '../base64'
decodeB64Byte, import { Base64BitflagIterator, EnumerateFlags, useIterator } from '../enumeration'
encodeB64Byte,
normaliseB64String,
ZERO_STRING,
} from '../base64'
import {
Base64BitflagIterator,
EnumerateFlags,
useIterator,
} from '../enumeration'
/** /**
* Provides flags that are stored in strings using a little-endian base 64 * Provides flags that are stored in strings using a little-endian base 64

View File

@@ -1,11 +1,7 @@
import type { FlagSet } from '.' import type { FlagSet } from '.'
import { UnavailableFeatureError } from '../errors' import { UnavailableFeatureError } from '../errors'
import { ENV_BI } from '../env' import { ENV_BI } from '../env'
import { import { BigBitFlagsIterator, EnumerateFlags, useIterator } from '../enumeration'
BigBitFlagsIterator,
EnumerateFlags,
useIterator,
} from '../enumeration'
export class BigBitFlagSet implements FlagSet<bigint, bigint> { export class BigBitFlagSet implements FlagSet<bigint, bigint> {
/** /**

View File

@@ -67,7 +67,7 @@ export interface FlagSet<F, S> {
/** /**
* Checks whether the first set of flags includes at least one of the flags * Checks whether the first set of flags includes at least one of the flags
* from the second set. * from the second set. If there is no required flags, the result will always be `false`.
* *
* A flag is considered to be part of the set only if all of its parents are * A flag is considered to be part of the set only if all of its parents are
* present too. * present too.
@@ -79,7 +79,7 @@ export interface FlagSet<F, S> {
/** /**
* Checks whether the first set of flags includes all the flags from the * Checks whether the first set of flags includes all the flags from the
* second set. * second set. If there is no required flags, the result will always be `true`.
* *
* A flag is considered to be part of the set only if all of its parents are * A flag is considered to be part of the set only if all of its parents are
* present too. * present too.

View File

@@ -1,6 +1,6 @@
import type { FlagSet } from '.' import type { FlagSet } from '.'
import { BitFlagsIterator, EnumerateFlags, useIterator } from '../enumeration' import { BitFlagsIterator, EnumerateFlags, useIterator } from '~/enumeration'
import { FlagDefinition, FlagsDictionary } from '../definitions' import { FlagDefinition, FlagsDictionary } from '~/definitions'
export class BitFlagSet implements FlagSet<number, number> { export class BitFlagSet implements FlagSet<number, number> {
private readonly _dictionary: FlagsDictionary<number, number> private readonly _dictionary: FlagsDictionary<number, number>
@@ -18,10 +18,7 @@ export class BitFlagSet implements FlagSet<number, number> {
} }
public named(...aliases: string[]): number { public named(...aliases: string[]): number {
return aliases.reduce( return aliases.reduce((set, alias) => set | (this.getFlag(alias)?.values ?? 0), 0)
(set, alias) => set | (this.getFlag(alias)?.values ?? 0),
0,
)
} }
public union(first: number, second: number): number { public union(first: number, second: number): number {
@@ -37,15 +34,15 @@ export class BitFlagSet implements FlagSet<number, number> {
} }
public isSuperset(first: number, second: number): boolean { public isSuperset(first: number, second: number): boolean {
return (first & second) == second return (first & second) === second
} }
public hasAny(flags: number, required: number): boolean { public hasAny(flags: number, required: number): boolean {
return false return this.minimum(this.intersection(flags, required)) !== 0
} }
public hasAll(flags: number, required: number): boolean { public hasAll(flags: number, required: number): boolean {
return false return this.isSuperset(flags, this.maximum(required))
} }
public enumerate(flags: number): EnumerateFlags<number> { public enumerate(flags: number): EnumerateFlags<number> {
@@ -53,11 +50,25 @@ export class BitFlagSet implements FlagSet<number, number> {
} }
public maximum(flags: number): number { public maximum(flags: number): number {
return 0 let result = this.none()
for (const value of this.enumerate(flags)) {
const flag = this._dictionary.findByValue(value)
if (flag !== undefined) {
result = flag.addTo(result)
}
}
return result
} }
public minimum(flags: number): number { public minimum(flags: number): number {
return 0 let result = this.none()
for (const value of this.enumerate(flags)) {
const flag = this._dictionary.findByValue(value)
if (flag !== undefined && flag.isIn(flags)) {
result = flag.addTo(result)
}
}
return result
} }
public getFlag(alias: string): FlagDefinition<number, number> | undefined { public getFlag(alias: string): FlagDefinition<number, number> | undefined {

View File

@@ -1,5 +1,5 @@
export { FlagSetBuilder, createBitFlagSet } from './builders' export { createBitFlagSet } from './builders'
export { FlagDefinition, FlagsDictionary } from './definitions' export { FlagDefinition } from './definitions'
export { InvalidBitFlagValueError } from './errors' export { InvalidBitFlagValueError } from './errors'
export { export {
ArrayFlagSet, ArrayFlagSet,

View File

@@ -1,49 +0,0 @@
import { FlagDefinition, FlagsDictionary } from '~'
import { describe, expect, test } from 'vitest'
import { ReusedFlagAliasError, ReusedFlagValueError } from '../../src/errors'
class TestDefinition implements FlagDefinition<unknown, unknown> {
private readonly _value: number
public constructor(value: number) {
this._value = value
}
public get values(): unknown {
return this._value
}
public hasSameValue(other: FlagDefinition<unknown, unknown>): boolean {
return other instanceof TestDefinition && this._value === other._value
}
}
describe(FlagsDictionary, () => {
test('define then look up', () => {
const dict = new FlagsDictionary()
const def = new TestDefinition(1)
dict.define('test', def)
expect(dict.findByAlias('test')).toBe(def)
expect(dict.findByAlias('undefined')).toBe(undefined)
})
test("can't use the same alias twice", () => {
const dict = new FlagsDictionary()
dict.define('test', new TestDefinition(1))
expect(() => dict.define('test', new TestDefinition(2))).toThrow(
ReusedFlagAliasError,
)
})
test("can't use the same value twice", () => {
const dict = new FlagsDictionary()
dict.define('test A', new TestDefinition(1))
expect(() => dict.define('test B', new TestDefinition(1))).toThrow(
ReusedFlagValueError,
)
})
})

View File

@@ -0,0 +1,52 @@
import { describe, expect, test } from 'vitest'
import { FlagsGraph } from '~/definitions/graph'
describe(FlagsGraph, () => {
test('put a definition in the graph', () => {
const graph = new FlagsGraph<number>()
const A = graph.put({ alias: 'A', value: 1 })
expect(graph.definitions).toHaveLength(1)
expect(graph.definitions).toContain(A)
expect(A.isPlaceholder).toBe(false)
expect(A.alias).toBe('A')
expect(A.value).toBe(1)
})
test('create a definition with a reference to an existing definition', () => {
const graph = new FlagsGraph<number>()
const A = graph.put({ alias: 'A', value: 1 })
expect(graph.definitions).toHaveLength(1)
expect(graph.definitions).toContain(A)
const B = graph.put({ alias: 'B', value: 2, parents: [{ alias: 'A' }] })
expect(graph.definitions).toHaveLength(2)
expect(graph.definitions).toContain(B)
expect(B.parents).toContain(A)
expect(A.children).toContain(B)
})
test('create a definition with a forward reference', () => {
const graph = new FlagsGraph<number>()
const B = graph.put({ alias: 'B', value: 2, parents: [{ alias: 'A' }] })
expect(graph.definitions).toHaveLength(1)
expect(graph.definitions).toContain(B)
const A = graph.put({ alias: 'A', value: 1 })
expect(graph.definitions).toHaveLength(2)
expect(graph.definitions).toContain(A)
expect(A.value).toBe(1)
expect(B.parents).toContain(A)
expect(A.children).toContain(B)
})
})

View File

@@ -36,10 +36,7 @@ describe(ArrayFlagSet, () => {
expect(flags.intersection(['A'], ['B'])).toEqual([]) expect(flags.intersection(['A'], ['B'])).toEqual([])
expect(flags.intersection(['A'], ['A', 'B'])).toEqual(['A']) expect(flags.intersection(['A'], ['A', 'B'])).toEqual(['A'])
expect(flags.intersection(['A', 'B', 'D'], ['A', 'C'])).toEqual(['A']) expect(flags.intersection(['A', 'B', 'D'], ['A', 'C'])).toEqual(['A'])
expect(flags.intersection(['A', 'B', 'D'], ['A', 'B', 'C'])).toEqual([ expect(flags.intersection(['A', 'B', 'D'], ['A', 'B', 'C'])).toEqual(['A', 'B'])
'A',
'B',
])
}) })
test('isSuperset', () => { test('isSuperset', () => {

View File

@@ -19,9 +19,7 @@ describe(CollectionFlagSet, () => {
expect(flags.union(set('A'), set())).toEqual(set('A')) expect(flags.union(set('A'), set())).toEqual(set('A'))
expect(flags.union(set(), set('B'))).toEqual(set('B')) expect(flags.union(set(), set('B'))).toEqual(set('B'))
expect(flags.union(set('A'), set('B'))).toEqual(set('A', 'B')) expect(flags.union(set('A'), set('B'))).toEqual(set('A', 'B'))
expect(flags.union(set('A', 'B'), set('B', 'C'))).toEqual( expect(flags.union(set('A', 'B'), set('B', 'C'))).toEqual(set('A', 'B', 'C'))
set('A', 'B', 'C'),
)
}) })
test('difference', () => { test('difference', () => {
@@ -41,12 +39,8 @@ describe(CollectionFlagSet, () => {
expect(flags.intersection(set('A'), set())).toEqual(set()) expect(flags.intersection(set('A'), set())).toEqual(set())
expect(flags.intersection(set('A'), set('B'))).toEqual(set()) expect(flags.intersection(set('A'), set('B'))).toEqual(set())
expect(flags.intersection(set('A'), set('A', 'B'))).toEqual(set('A')) expect(flags.intersection(set('A'), set('A', 'B'))).toEqual(set('A'))
expect(flags.intersection(set('A', 'B', 'D'), set('A', 'C'))).toEqual( expect(flags.intersection(set('A', 'B', 'D'), set('A', 'C'))).toEqual(set('A'))
set('A'), expect(flags.intersection(set('A', 'B', 'D'), set('A', 'B', 'C'))).toEqual(set('A', 'B'))
)
expect(
flags.intersection(set('A', 'B', 'D'), set('A', 'B', 'C')),
).toEqual(set('A', 'B'))
}) })
test('isSuperset', () => { test('isSuperset', () => {
@@ -65,10 +59,6 @@ describe(CollectionFlagSet, () => {
expect([...flags.enumerate(set())]).toEqual([]) expect([...flags.enumerate(set())]).toEqual([])
expect([...flags.enumerate(set('A'))]).toEqual(['A']) expect([...flags.enumerate(set('A'))]).toEqual(['A'])
expect([...flags.enumerate(set('A', 'B', 'C'))]).toEqual([ expect([...flags.enumerate(set('A', 'B', 'C'))]).toEqual(['A', 'B', 'C'])
'A',
'B',
'C',
])
}) })
}) })

View File

@@ -75,6 +75,38 @@ describe(BitFlagSet, () => {
expect(flags.isSuperset(8, 4)).toBe(false) expect(flags.isSuperset(8, 4)).toBe(false)
}) })
test('hasAny', () => {
const flags = createBitFlagSet([
{ value: 1, as: 'A' },
{ value: 2, as: 'B', requires: ['A'] },
{ value: 4, as: 'C', requires: ['A'] },
{ value: 8, as: 'D', requires: ['B', 'C'] },
])
expect(flags.hasAny(15, 0)).toBe(false)
expect(flags.hasAny(0, 0)).toBe(false)
expect(flags.hasAny(7, 1)).toBe(true)
expect(flags.hasAny(1, 7)).toBe(true)
expect(flags.hasAny(5, 12)).toBe(false)
expect(flags.hasAny(2, 2)).toBe(false)
})
test('hasAll', () => {
const flags = createBitFlagSet([
{ value: 1, as: 'A' },
{ value: 2, as: 'B', requires: ['A'] },
{ value: 4, as: 'C', requires: ['A'] },
{ value: 8, as: 'D', requires: ['B', 'C'] },
])
expect(flags.hasAll(15, 0)).toBe(true)
expect(flags.hasAll(0, 0)).toBe(true)
expect(flags.hasAll(7, 2)).toBe(true)
expect(flags.hasAll(6, 2)).toBe(false)
expect(flags.hasAll(1, 7)).toBe(false)
expect(flags.hasAll(5, 12)).toBe(false)
})
test('enumerate', () => { test('enumerate', () => {
const flags = createBitFlagSet([]) const flags = createBitFlagSet([])
@@ -85,4 +117,36 @@ describe(BitFlagSet, () => {
expect([...flags.enumerate(11)]).toEqual([1, 2, 8]) expect([...flags.enumerate(11)]).toEqual([1, 2, 8])
expect([...flags.enumerate(100)]).toEqual([4, 32, 64]) expect([...flags.enumerate(100)]).toEqual([4, 32, 64])
}) })
test('maximum', () => {
const flags = createBitFlagSet([
{ value: 1, as: 'A' },
{ value: 2, as: 'B', requires: ['A'] },
{ value: 4, as: 'C', requires: ['A'] },
{ value: 8, as: 'D', requires: ['B', 'C'] },
])
expect(flags.maximum(0)).toEqual(0)
expect(flags.maximum(1)).toEqual(1)
expect(flags.maximum(2)).toEqual(3)
expect(flags.maximum(3)).toEqual(3)
expect(flags.maximum(4)).toEqual(5)
expect(flags.maximum(8)).toEqual(15)
})
test('minimum', () => {
const flags = createBitFlagSet([
{ value: 1, as: 'A' },
{ value: 2, as: 'B', requires: ['A'] },
{ value: 4, as: 'C', requires: ['A'] },
{ value: 8, as: 'D', requires: ['B', 'C'] },
])
expect(flags.minimum(0)).toEqual(0)
expect(flags.minimum(1)).toEqual(1)
expect(flags.minimum(2)).toEqual(0)
expect(flags.minimum(3)).toEqual(3)
expect(flags.minimum(4)).toEqual(0)
expect(flags.minimum(13)).toEqual(5)
})
}) })

View File

@@ -1,111 +1,113 @@
{ {
"compilerOptions": { "compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */ /* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */ /* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */ /* Language and Environment */
"target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */ // "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */ /* Modules */
"module": "commonjs", /* Specify what module code is generated. */ "module": "commonjs" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */ // "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */ "paths": {
"~": ["./src/index.ts"] /* Specify a set of entries that re-map imports to additional lookup locations. */
}, "~": ["./src/index.ts"],
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ "~/*": ["./src/*"]
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ },
// "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */ "resolveJsonModule": true /* Enable importing .json files. */,
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */ /* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */ /* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */ "outDir": "./dist" /* Specify an output folder for all emitted files. */,
// "removeComments": true, /* Disable emitting comments. */ // "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */ // "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */ // "newLine": "crlf", /* Set the newline character for emitting files. */
"stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ "stripInternal": true /* Disable emitting declarations that have '@internal' in their JSDoc comments. */,
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */ /* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */ /* Type Checking */
"strict": true, /* Enable all strict type-checking options. */ "strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
"noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ "noImplicitThis": true /* Enable error reporting when 'this' is given the type 'any'. */,
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
"noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ "noImplicitOverride": true /* Ensure overriding members in derived classes are marked with an override modifier. */,
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */ /* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */
} }
} }

View File

@@ -3,7 +3,7 @@ import { defineConfig } from 'vitest/config'
export default defineConfig({ export default defineConfig({
test: { test: {
alias: { alias: {
'~': 'src/index.ts', '~': 'src',
}, },
}, },
}) })