@bessemer/cornerstone 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/jest.config.js +3 -0
  2. package/package.json +39 -0
  3. package/src/array.ts +142 -0
  4. package/src/async.ts +114 -0
  5. package/src/cache.ts +236 -0
  6. package/src/combinable.ts +40 -0
  7. package/src/comparator.ts +78 -0
  8. package/src/content.ts +138 -0
  9. package/src/context.ts +6 -0
  10. package/src/crypto.ts +11 -0
  11. package/src/date.ts +18 -0
  12. package/src/duration.ts +57 -0
  13. package/src/either.ts +29 -0
  14. package/src/entry.ts +21 -0
  15. package/src/equalitor.ts +12 -0
  16. package/src/error-event.ts +126 -0
  17. package/src/error.ts +16 -0
  18. package/src/expression/array-expression.ts +29 -0
  19. package/src/expression/expression-evaluator.ts +34 -0
  20. package/src/expression/expression.ts +188 -0
  21. package/src/expression/internal.ts +34 -0
  22. package/src/expression/numeric-expression.ts +182 -0
  23. package/src/expression/string-expression.ts +38 -0
  24. package/src/expression.ts +48 -0
  25. package/src/function.ts +3 -0
  26. package/src/glob.ts +19 -0
  27. package/src/global-variable.ts +40 -0
  28. package/src/hash.ts +28 -0
  29. package/src/hex-code.ts +6 -0
  30. package/src/index.ts +82 -0
  31. package/src/lazy.ts +11 -0
  32. package/src/logger.ts +144 -0
  33. package/src/math.ts +132 -0
  34. package/src/misc.ts +22 -0
  35. package/src/object.ts +236 -0
  36. package/src/patch.ts +128 -0
  37. package/src/precondition.ts +25 -0
  38. package/src/promise.ts +16 -0
  39. package/src/property.ts +29 -0
  40. package/src/reference.ts +68 -0
  41. package/src/resource.ts +32 -0
  42. package/src/result.ts +66 -0
  43. package/src/retry.ts +70 -0
  44. package/src/rich-text.ts +24 -0
  45. package/src/set.ts +46 -0
  46. package/src/signature.ts +20 -0
  47. package/src/store.ts +91 -0
  48. package/src/string.ts +173 -0
  49. package/src/tag.ts +68 -0
  50. package/src/types.ts +21 -0
  51. package/src/ulid.ts +28 -0
  52. package/src/unit.ts +4 -0
  53. package/src/uri.ts +321 -0
  54. package/src/url.ts +155 -0
  55. package/src/uuid.ts +37 -0
  56. package/src/zod.ts +24 -0
  57. package/test/comparator.test.ts +1 -0
  58. package/test/expression.test.ts +12 -0
  59. package/test/object.test.ts +104 -0
  60. package/test/patch.test.ts +170 -0
  61. package/test/set.test.ts +20 -0
  62. package/test/string.test.ts +22 -0
  63. package/test/uri.test.ts +111 -0
  64. package/test/url.test.ts +174 -0
  65. package/tsconfig.build.json +13 -0
  66. package/tsup.config.ts +4 -0
@@ -0,0 +1,29 @@
1
+ import { Objects, Tags } from '@bessemer/cornerstone'
2
+ import { SerializedTags, Tag, TaggedValue } from '@bessemer/cornerstone/tag'
3
+ import { UnknownRecord } from 'type-fest'
4
+ import { DeepPartial } from '@bessemer/cornerstone/types'
5
+
6
+ export type PropertyRecord<T extends UnknownRecord> = {
7
+ values: T
8
+ overrides: Record<SerializedTags, PropertyOverride<T>>
9
+ }
10
+
11
+ export type PropertyOverride<T> = TaggedValue<DeepPartial<T>>
12
+
13
+ export const properties = <T extends UnknownRecord>(values: T, overrides?: Array<PropertyOverride<T>>): PropertyRecord<T> => {
14
+ const propertyOverrideEntries = (overrides ?? []).map((override) => {
15
+ return [Tags.serializeTags(override.tags), override]
16
+ })
17
+
18
+ const propertyOverrides: Record<SerializedTags, PropertyOverride<T>> = Object.fromEntries(propertyOverrideEntries)
19
+
20
+ return {
21
+ values,
22
+ overrides: propertyOverrides,
23
+ }
24
+ }
25
+
26
+ export const resolve = <T extends UnknownRecord>(properties: PropertyRecord<T>, tags: Array<Tag>): T => {
27
+ const overrides = Tags.resolve(Object.values(properties.overrides), tags)
28
+ return Objects.mergeAll([properties.values, ...overrides.reverse()]) as T
29
+ }
@@ -0,0 +1,68 @@
1
+ import { Comparators, Equalitors, Objects, Strings } from '@bessemer/cornerstone'
2
+ import { Comparator } from '@bessemer/cornerstone/comparator'
3
+ import { NominalType } from '@bessemer/cornerstone/types'
4
+
5
+ export type ReferenceId<T extends string> = NominalType<string, ['ReferenceId', T]>
6
+
7
+ export type Reference<T extends string> = {
8
+ id: ReferenceId<T>
9
+ type: T
10
+ note?: string
11
+ }
12
+
13
+ export type ReferenceType<T extends Reference<string>> = T | T['id']
14
+
15
+ export interface Referencable<T extends Reference<string>> {
16
+ reference: T
17
+ }
18
+
19
+ export type ReferencableType<T extends Reference<string>> = T | Referencable<T>
20
+
21
+ export const reference = <T extends string>(reference: Reference<T> | ReferenceId<T>, type: T, note?: string): Reference<typeof type> => {
22
+ if (!Strings.isString(reference)) {
23
+ return reference
24
+ }
25
+
26
+ return {
27
+ id: reference,
28
+ type,
29
+ ...(Objects.isPresent(note) ? { note: note } : {}),
30
+ }
31
+ }
32
+
33
+ export const isReferencable = (element: unknown): element is Referencable<Reference<string>> => {
34
+ if (!Objects.isObject(element)) {
35
+ return false
36
+ }
37
+
38
+ const referencable = element as Referencable<Reference<string>>
39
+ return !Objects.isUndefined(referencable.reference)
40
+ }
41
+
42
+ export const isReference = (element: unknown): element is Reference<string> => {
43
+ if (!Objects.isObject(element)) {
44
+ return false
45
+ }
46
+
47
+ const referencable = element as Reference<string>
48
+ return !Objects.isUndefined(referencable.id) && !Objects.isUndefined(referencable.type) && !Objects.isUndefined(referencable.note)
49
+ }
50
+
51
+ export const getReference = <T extends Reference<string>>(reference: ReferencableType<T>): T => {
52
+ const referencable = reference as Referencable<T>
53
+ if (Objects.isPresent(referencable.reference)) {
54
+ return referencable.reference
55
+ } else {
56
+ return reference as T
57
+ }
58
+ }
59
+
60
+ export const equals = <T extends string>(first: Reference<T>, second: Reference<T>): boolean => {
61
+ return first.id === second.id
62
+ }
63
+
64
+ export const comparator = <T extends string>(): Comparator<Reference<T>> => {
65
+ return Comparators.compareBy((it) => it.id, Comparators.natural())
66
+ }
67
+
68
+ export const equalitor = () => Equalitors.fromComparator(comparator())
@@ -0,0 +1,32 @@
1
+ import { NominalType } from '@bessemer/cornerstone/types'
2
+ import { Strings, Zod } from '@bessemer/cornerstone/index'
3
+ import { ZodType } from 'zod'
4
+
5
+ export type ResourceKey = string
6
+
7
+ export type ResourceNamespace = NominalType<string, 'ResourceNamespace'>
8
+ export const ResourceNamespaceSchema: ZodType<ResourceNamespace> = Zod.string()
9
+
10
+ export namespace ResourceKey {
11
+ const ResourceNamespaceSeparator = '/'
12
+
13
+ export const namespace = (namespace: ResourceNamespace, key: ResourceKey): ResourceKey => {
14
+ return `${namespace}${ResourceNamespaceSeparator}${key}`
15
+ }
16
+
17
+ export const namespaceAll = (namespace: ResourceNamespace, keys: Array<ResourceKey>): Array<ResourceKey> => {
18
+ return keys.map((it) => ResourceKey.namespace(namespace, it))
19
+ }
20
+
21
+ export const stripNamespace = (namespace: ResourceNamespace, key: ResourceKey): ResourceKey => {
22
+ return Strings.removeStart(key, `${namespace}${ResourceNamespaceSeparator}`)
23
+ }
24
+
25
+ export const stripNamespaceAll = (namespace: ResourceNamespace, keys: Array<ResourceKey>): Array<ResourceKey> => {
26
+ return keys.map((it) => ResourceKey.stripNamespace(namespace, it))
27
+ }
28
+
29
+ export const extendNamespace = (...namespaces: Array<ResourceNamespace>): ResourceNamespace => {
30
+ return namespaces.join(ResourceNamespaceSeparator)
31
+ }
32
+ }
package/src/result.ts ADDED
@@ -0,0 +1,66 @@
1
+ import { Left, Right } from '@bessemer/cornerstone/either'
2
+ import { Eithers, Promises } from '@bessemer/cornerstone'
3
+ import { Throwable } from '@bessemer/cornerstone/types'
4
+
5
+ export type Success<T> = Right<T> & {
6
+ isSuccess: true
7
+ }
8
+
9
+ export type Failure = Left<Throwable | null> & {
10
+ isSuccess: false
11
+ }
12
+
13
+ export type Result<T> = Success<T> | Failure
14
+ export type AsyncResult<T> = Promise<Result<T>>
15
+
16
+ export const success = <T>(value: T): Success<T> => {
17
+ return { ...Eithers.right(value), isSuccess: true }
18
+ }
19
+
20
+ export const failure = (failure?: Throwable): Failure => {
21
+ return { ...Eithers.left(failure ?? null), isSuccess: false }
22
+ }
23
+
24
+ export function tryValue<SOURCE_VALUE>(resolver: () => Promise<SOURCE_VALUE>): AsyncResult<SOURCE_VALUE>
25
+ export function tryValue<SOURCE_VALUE>(resolver: () => SOURCE_VALUE): Result<SOURCE_VALUE>
26
+ export function tryValue<SOURCE_VALUE>(resolver: () => SOURCE_VALUE | Promise<SOURCE_VALUE>): Result<SOURCE_VALUE> | Promise<Result<SOURCE_VALUE>> {
27
+ try {
28
+ let result = resolver()
29
+ if (Promises.isPromise(result)) {
30
+ return result.then((it) => success(it)).catch((it) => failure(it))
31
+ } else {
32
+ return success(result)
33
+ }
34
+ } catch (e: any) {
35
+ return failure(e)
36
+ }
37
+ }
38
+
39
+ export function tryResult<SOURCE_VALUE>(resolver: () => Result<SOURCE_VALUE>): Result<SOURCE_VALUE>
40
+ export function tryResult<SOURCE_VALUE>(resolver: () => AsyncResult<SOURCE_VALUE>): AsyncResult<SOURCE_VALUE>
41
+ export function tryResult<SOURCE_VALUE>(resolver: () => Result<SOURCE_VALUE> | AsyncResult<SOURCE_VALUE>): ReturnType<typeof resolver> {
42
+ try {
43
+ let result = resolver()
44
+ if (Promises.isPromise(result)) {
45
+ return result.catch((it) => failure(it))
46
+ } else {
47
+ return result
48
+ }
49
+ } catch (e: any) {
50
+ return failure(e)
51
+ }
52
+ }
53
+
54
+ export function map<SOURCE_VALUE, TARGET_VALUE>(
55
+ result: Result<SOURCE_VALUE>,
56
+ valueMapper: (element: SOURCE_VALUE) => TARGET_VALUE
57
+ ): Result<TARGET_VALUE> {
58
+ if (result.isSuccess) {
59
+ return {
60
+ ...result,
61
+ value: valueMapper(result.value),
62
+ }
63
+ } else {
64
+ return result
65
+ }
66
+ }
package/src/retry.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { Duration } from '@bessemer/cornerstone/duration'
2
+ import { Async, Durations, Maths, Objects, Preconditions, Results } from '@bessemer/cornerstone/index'
3
+ import { AsyncResult, Result } from '@bessemer/cornerstone/result'
4
+ import { PartialDeep } from 'type-fest'
5
+
6
+ export type RetryProps = {
7
+ attempts: number
8
+ delay: Duration
9
+ }
10
+
11
+ export type RetryOptions = PartialDeep<RetryProps>
12
+
13
+ export const None: RetryProps = {
14
+ attempts: 0,
15
+ delay: Durations.Zero,
16
+ }
17
+
18
+ export const DefaultRetryProps: RetryProps = {
19
+ attempts: 3,
20
+ delay: Durations.ofMilliseconds(500),
21
+ }
22
+
23
+ export type RetryState = {
24
+ attempt: number
25
+ props: RetryProps
26
+ }
27
+
28
+ export const initialize = (initialOptions?: RetryOptions): RetryState => {
29
+ const props = Objects.merge(DefaultRetryProps, initialOptions)
30
+ Preconditions.isTrue(props.attempts >= 0, () => 'usingRetry attempts must be >= 0')
31
+
32
+ return {
33
+ attempt: 0,
34
+ props,
35
+ }
36
+ }
37
+
38
+ export const retry = async (state: RetryState): Promise<RetryState | undefined> => {
39
+ if (state.attempt >= state.props.attempts - 1) {
40
+ return undefined
41
+ }
42
+
43
+ const delayMs = Durations.inMilliseconds(state.props.delay)
44
+ const maxJitterMs = delayMs * 0.3 // We calculate max jitter as 30% of the delay
45
+ await Async.sleep(Durations.ofMilliseconds(delayMs + Maths.random(0, maxJitterMs)))
46
+
47
+ return {
48
+ props: state.props,
49
+ attempt: state.attempt + 1,
50
+ }
51
+ }
52
+
53
+ export const usingRetry = async <T>(runnable: () => Promise<Result<T>>, initialOptions?: RetryOptions): AsyncResult<T> => {
54
+ let retryState: RetryState | undefined = initialize(initialOptions)
55
+ let previousResult: Result<T> = Results.failure()
56
+
57
+ do {
58
+ // JOHN Should this be a try/catch? it was causing debugging problems
59
+ const result = await runnable()
60
+ previousResult = result
61
+
62
+ if (result.isSuccess) {
63
+ return result
64
+ }
65
+
66
+ retryState = await retry(retryState)
67
+ } while (!Objects.isUndefined(retryState))
68
+
69
+ return previousResult
70
+ }
@@ -0,0 +1,24 @@
1
+ import { NominalType } from '@bessemer/cornerstone/types'
2
+ import { Objects } from '@bessemer/cornerstone'
3
+
4
+ // These are to match TipTap types, but without us having to depend on the TipTap library
5
+ export type RichTextJson = {
6
+ type?: string
7
+ attrs?: Record<string, any>
8
+ content?: RichTextJson[]
9
+ marks?: {
10
+ type: string
11
+ attrs?: Record<string, any>
12
+ [key: string]: any
13
+ }[]
14
+ text?: string
15
+ [key: string]: any
16
+ }
17
+
18
+ export type RichTextString = NominalType<string, 'RichTextString'>
19
+
20
+ export type RichText = RichTextString | RichTextJson
21
+
22
+ export const isJson = (text: RichText): text is RichTextJson => {
23
+ return Objects.isObject(text)
24
+ }
package/src/set.ts ADDED
@@ -0,0 +1,46 @@
1
+ export const cartesianProduct = <T>(...arrays: Array<Array<T>>): Array<Array<T>> => {
2
+ return arrays.reduce<Array<Array<T>>>((acc, array) => acc.flatMap((product) => array.map((element) => [...product, element])), [[]])
3
+ }
4
+
5
+ export const permute = <T>(values: Array<T>): Array<Array<T>> => {
6
+ let result: Array<Array<T>> = []
7
+
8
+ const permuteInternal = (arr: Array<T>, m: Array<T> = []) => {
9
+ if (arr.length === 0) {
10
+ result.push(m)
11
+ } else {
12
+ for (let i = 0; i < arr.length; i++) {
13
+ let curr = arr.slice()
14
+ let next = curr.splice(i, 1)
15
+ permuteInternal(curr.slice(), m.concat(next))
16
+ }
17
+ }
18
+ }
19
+
20
+ permuteInternal(values)
21
+ return result
22
+ }
23
+
24
+ export const properPowerSet = <T>(values: Array<T>): Array<Array<T>> => {
25
+ const powerSet: Array<Array<T>> = []
26
+
27
+ const totalSubsets = 1 << values.length // 2^n, where n is the size of the set
28
+
29
+ for (let i = 1; i < totalSubsets; i++) {
30
+ // Start from 1 to exclude the empty set
31
+ const subset: T[] = []
32
+ for (let j = 0; j < values.length; j++) {
33
+ if (i & (1 << j)) {
34
+ // Check if the j-th element is in the subset
35
+ subset.push(values[j]!)
36
+ }
37
+ }
38
+ powerSet.push(subset)
39
+ }
40
+
41
+ return powerSet
42
+ }
43
+
44
+ export const powerSet = <T>(values: Array<T>): Array<Array<T>> => {
45
+ return [[], ...properPowerSet(values)]
46
+ }
@@ -0,0 +1,20 @@
1
+ import { Objects, References } from '@bessemer/cornerstone'
2
+ import { Reference } from '@bessemer/cornerstone/reference'
3
+
4
+ export type Signable = number | string | { id: string } | { reference: Reference<string> }
5
+
6
+ export const sign = (value: Signable): string | number => {
7
+ if (Objects.isObject(value)) {
8
+ if (References.isReferencable(value)) {
9
+ return value.reference.id
10
+ } else {
11
+ return value.id
12
+ }
13
+ }
14
+
15
+ return value
16
+ }
17
+
18
+ export const signAll = (values: Array<Signable>): Array<string | number> => {
19
+ return values.map(sign)
20
+ }
package/src/store.ts ADDED
@@ -0,0 +1,91 @@
1
+ import { ResourceKey } from '@bessemer/cornerstone/resource'
2
+ import { Entry } from '@bessemer/cornerstone/entry'
3
+ import { Arrays, Entries, Objects } from '@bessemer/cornerstone/index'
4
+
5
+ export interface LocalStore<T> {
6
+ setValue: (value: T | undefined) => void
7
+ getValue: () => T | undefined
8
+ }
9
+
10
+ export interface LocalKeyValueStore<T> {
11
+ setValues: (entries: Array<Entry<T | undefined>>) => void
12
+ setValue: (key: ResourceKey, value: T | undefined) => void
13
+
14
+ getEntries: () => Array<Entry<T>>
15
+ getValues: (keys: Array<ResourceKey>) => Array<Entry<T>>
16
+ getValue: (key: ResourceKey) => T | undefined
17
+ }
18
+
19
+ export interface RemoteStore<T> {
20
+ writeValue: (value: T | undefined) => Promise<void>
21
+ fetchValue: () => Promise<T | undefined>
22
+ }
23
+
24
+ export interface RemoteKeyValueStore<T> {
25
+ writeValues: (entries: Array<Entry<T | undefined>>) => Promise<void>
26
+ writeValue: (key: ResourceKey, value: T | undefined) => Promise<void>
27
+
28
+ fetchValues: (keys: Array<ResourceKey>) => Promise<Array<Entry<T>>>
29
+ fetchValue: (key: ResourceKey) => Promise<T | undefined>
30
+ }
31
+
32
+ export abstract class AbstractRemoteKeyValueStore<T> implements RemoteKeyValueStore<T> {
33
+ abstract writeValues: (entries: Array<Entry<T | undefined>>) => Promise<void>
34
+
35
+ writeValue = async (key: ResourceKey, value: T | undefined): Promise<void> => {
36
+ await this.writeValues([Entries.of(key, value)])
37
+ }
38
+
39
+ abstract fetchValues: (keys: Array<ResourceKey>) => Promise<Array<Entry<T>>>
40
+
41
+ fetchValue = async (key: ResourceKey): Promise<T | undefined> => {
42
+ return Arrays.first(await this.fetchValues([key]))?.[1]
43
+ }
44
+ }
45
+
46
+ export abstract class AbstractLocalKeyValueStore<T> extends AbstractRemoteKeyValueStore<T> implements LocalKeyValueStore<T> {
47
+ abstract setValues: (entries: Array<Entry<T | undefined>>) => void
48
+
49
+ setValue = (key: ResourceKey, value: T | undefined): void => {
50
+ this.setValues([Entries.of(key, value)])
51
+ }
52
+
53
+ abstract getEntries: () => Array<Entry<T>>
54
+ abstract getValues: (keys: Array<ResourceKey>) => Array<Entry<T>>
55
+
56
+ getValue = (key: ResourceKey): T | undefined => {
57
+ return Arrays.first(this.getValues([key]))?.[1]
58
+ }
59
+
60
+ fetchValues = async (keys: Array<ResourceKey>): Promise<Array<Entry<T>>> => {
61
+ return this.getValues(keys)
62
+ }
63
+
64
+ writeValues = async (entries: Array<Entry<T | undefined>>): Promise<void> => {
65
+ this.setValues(entries)
66
+ }
67
+ }
68
+
69
+ export const fromMap = <T>(): LocalKeyValueStore<T> => {
70
+ const map = new Map<string, T>()
71
+
72
+ return new (class extends AbstractLocalKeyValueStore<T> {
73
+ override getEntries = (): Array<Entry<T>> => {
74
+ return Array.from(map.entries())
75
+ }
76
+
77
+ override setValues = (entries: Entry<T | undefined>[]): void => {
78
+ entries.forEach(([key, value]) => {
79
+ if (Objects.isNil(value)) {
80
+ map.delete(key)
81
+ } else {
82
+ map.set(key, value)
83
+ }
84
+ })
85
+ }
86
+
87
+ override getValues = (keys: Array<ResourceKey>): Array<Entry<T>> => {
88
+ return keys.map((key) => Entries.of(key, map.get(key)!)).filter((it) => Objects.isPresent(it[1]))
89
+ }
90
+ })()
91
+ }
package/src/string.ts ADDED
@@ -0,0 +1,173 @@
1
+ import {
2
+ endsWith as _endsWith,
3
+ includes as _includes,
4
+ isEmpty as _isEmpty,
5
+ isString as _isString,
6
+ padEnd as _padEnd,
7
+ padStart as _padStart,
8
+ replace as _replace,
9
+ startsWith as _startsWith,
10
+ } from 'lodash-es'
11
+ import { Arrays } from '@bessemer/cornerstone'
12
+ import { UnknownRecord } from 'type-fest'
13
+
14
+ export const isString = (value?: any): value is string => {
15
+ return _isString(value)
16
+ }
17
+
18
+ export const isEmpty = _isEmpty
19
+
20
+ export type StringSplitResult = { selection: string; separator: string; rest: string } | { selection: null; separator: null; rest: string }
21
+
22
+ export const splitFirst = (str: string, splitter: string | RegExp): StringSplitResult => {
23
+ if (isString(splitter)) {
24
+ const results = str.split(splitter)
25
+ if (results.length === 1) {
26
+ return { selection: null, separator: null, rest: str }
27
+ }
28
+
29
+ return { selection: results[0]!, separator: splitter, rest: Arrays.rest(results).join(splitter) }
30
+ } else {
31
+ const match = splitter.exec(str)
32
+
33
+ if (!match) {
34
+ return { selection: null, separator: null, rest: str }
35
+ }
36
+
37
+ const matchIndex = match.index
38
+ const beforeMatch = str.slice(0, matchIndex)
39
+ const afterMatch = str.slice(matchIndex + match[0].length)
40
+ const separator = match[0]
41
+ return { selection: beforeMatch, separator, rest: afterMatch }
42
+ }
43
+ }
44
+
45
+ export const splitLast = (str: string, splitter: string | RegExp): StringSplitResult => {
46
+ if (isString(splitter)) {
47
+ const results = str.split(splitter)
48
+ if (results.length === 1) {
49
+ return { selection: null, separator: null, rest: str }
50
+ }
51
+
52
+ return { selection: results[results.length - 1]!, separator: splitter, rest: results.slice(0, results.length - 1).join(splitter) }
53
+ } else {
54
+ if (!splitter.global) {
55
+ splitter = new RegExp(splitter.source, splitter.flags + 'g')
56
+ }
57
+
58
+ const matches = Array.from(str.matchAll(splitter))
59
+
60
+ if (matches.length === 0) {
61
+ return { selection: null, separator: null, rest: str }
62
+ }
63
+
64
+ // Use the last match
65
+ const lastMatch = matches[matches.length - 1]!
66
+ const matchIndex = lastMatch.index!
67
+ const separator = lastMatch[0]!
68
+ const beforeMatch = str.slice(0, matchIndex)
69
+ const afterMatch = str.slice(matchIndex + separator.length)
70
+
71
+ return {
72
+ selection: afterMatch,
73
+ separator: separator,
74
+ rest: beforeMatch,
75
+ }
76
+ }
77
+ }
78
+
79
+ export const splitLastRegex = (str: string, regex: RegExp): StringSplitResult => {
80
+ // Find the last match using regex lastIndex
81
+ let lastMatch: RegExpExecArray | null = null
82
+ let match
83
+
84
+ while ((match = regex.exec(str)) !== null) {
85
+ lastMatch = match
86
+ }
87
+
88
+ if (!lastMatch) {
89
+ return { selection: null, separator: null, rest: str }
90
+ }
91
+
92
+ const matchIndex = lastMatch.index!
93
+ const separator = lastMatch[0]
94
+ const beforeMatch = str.slice(0, matchIndex)
95
+ const afterMatch = str.slice(matchIndex + separator.length)
96
+
97
+ return {
98
+ selection: afterMatch,
99
+ separator,
100
+ rest: beforeMatch,
101
+ }
102
+ }
103
+
104
+ export const splitAt = (str: string, index: number): [string, string] => {
105
+ return [str.slice(0, index), str.slice(index)] as const
106
+ }
107
+
108
+ export const startsWith = _startsWith
109
+ export const endsWith = _endsWith
110
+
111
+ export const removeStart = (string: string, substring: string): string => {
112
+ if (!string.startsWith(substring)) {
113
+ return string
114
+ }
115
+
116
+ return string.slice(substring.length)
117
+ }
118
+
119
+ export const removeEnd = (string: string, substring: string): string => {
120
+ if (!string.endsWith(substring)) {
121
+ return string
122
+ }
123
+
124
+ return string.slice(0, -substring.length)
125
+ }
126
+
127
+ export const isBlank = (str?: string | null): boolean => {
128
+ const testStr = str ?? ''
129
+ return /^\s*$/.test(testStr)
130
+ }
131
+
132
+ export const mostCentralOccurrence = (str: string, substr: string): number | null => {
133
+ const occurrences: number[] = []
134
+ let index = str.indexOf(substr)
135
+
136
+ while (index !== -1) {
137
+ occurrences.push(index)
138
+ index = str.indexOf(substr, index + 1)
139
+ }
140
+
141
+ if (occurrences.length === 0) {
142
+ return null
143
+ }
144
+
145
+ const center = str.length / 2
146
+
147
+ let closestIndex = occurrences[0]!
148
+ let minDistance = Math.abs(center - closestIndex)
149
+
150
+ for (let i = 1; i < occurrences.length; i++) {
151
+ const distance = Math.abs(center - occurrences[i]!)
152
+ if (distance < minDistance) {
153
+ minDistance = distance
154
+ closestIndex = occurrences[i]!
155
+ }
156
+ }
157
+
158
+ return closestIndex
159
+ }
160
+
161
+ export const replacePlaceholders = (string: string, parameters: UnknownRecord, getParamPlaceholder = (paramName: string) => `{{${paramName}}}`) => {
162
+ return Object.entries(parameters).reduce(
163
+ (intermediateString, [paramName, paramValue]) => intermediateString.replaceAll(getParamPlaceholder(paramName), `${paramValue}`),
164
+ string
165
+ )
166
+ }
167
+
168
+ export const padStart = _padStart
169
+ export const padEnd = _padEnd
170
+
171
+ export const contains = _includes
172
+
173
+ export const replace = _replace
package/src/tag.ts ADDED
@@ -0,0 +1,68 @@
1
+ import { NominalType } from '@bessemer/cornerstone/types'
2
+ import { Comparator } from '@bessemer/cornerstone/comparator'
3
+ import { Arrays, Comparators, Equalitors, Sets } from '@bessemer/cornerstone'
4
+ import { Equalitor } from '@bessemer/cornerstone/equalitor'
5
+
6
+ export type TagType<DataType> = NominalType<string, ['TagType', DataType]>
7
+
8
+ export type Tag<DataType = unknown> = {
9
+ type: TagType<DataType>
10
+ value: DataType
11
+ }
12
+
13
+ export type TaggedValue<T> = {
14
+ value: T
15
+ tags: Array<Tag>
16
+ }
17
+
18
+ export const tag = <T>(type: TagType<T>, value: T): Tag<T> => {
19
+ return {
20
+ type,
21
+ value,
22
+ }
23
+ }
24
+
25
+ export const value = <T>(value: T, tags: Array<Tag>) => {
26
+ return {
27
+ value,
28
+ tags: Arrays.sortWith(tags, tagComparator()),
29
+ }
30
+ }
31
+
32
+ export const tagComparator = <T>(): Comparator<Tag<T>> => {
33
+ return Comparators.aggregate([
34
+ Comparators.compareBy((it) => it.type, Comparators.natural()),
35
+ Comparators.compareBy((it) => JSON.stringify(it.value), Comparators.natural()),
36
+ ])
37
+ }
38
+
39
+ export const tagEqualitor = <T>(): Equalitor<Tag<T>> => {
40
+ return Equalitors.fromComparator(tagComparator())
41
+ }
42
+
43
+ export type SerializedTags = NominalType<string, 'SerializedTags'>
44
+
45
+ export const serializeTags = <T>(tags: Array<Tag<T>>): SerializedTags => {
46
+ const serializedTags: SerializedTags = Arrays.sortWith(tags, tagComparator())
47
+ .map(({ type, value }) => `${type}:${JSON.stringify(value)}`)
48
+ .join('.')
49
+
50
+ return serializedTags
51
+ }
52
+
53
+ export const resolve = <T>(values: Array<TaggedValue<T>>, tags: Array<Tag>): Array<TaggedValue<T>> => {
54
+ return resolveBy(values, (it) => it.tags, tags)
55
+ }
56
+
57
+ export const resolveBy = <T>(values: Array<T>, mapper: (value: T) => Array<Tag>, tags: Array<Tag>): Array<T> => {
58
+ const resolvedValues = Sets.properPowerSet(tags).flatMap((tags) => {
59
+ return values.filter((it) => Arrays.equalWith(mapper(it), tags, tagEqualitor()))
60
+ })
61
+
62
+ if (Arrays.isEmpty(resolvedValues)) {
63
+ const defaultValues = values.filter((it) => Arrays.isEmpty(mapper(it)))
64
+ return defaultValues
65
+ }
66
+
67
+ return resolvedValues.reverse()
68
+ }