@lionweb/ts-utils 0.7.0-beta.18 → 0.7.0-beta.19

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.
package/CHANGELOG.md CHANGED
@@ -5,4 +5,5 @@
5
5
  * Initial creation and publication of this package, as an extraction and de-duplication from `@lionweb/core`, `@lionweb/utilities`, `@lionweb/class-core`, and `@lionweb/validation`.
6
6
  * Add 'mapFrom' function that maps an array to a map, using given key and value functions.
7
7
  * Introduce explicit types for `nested{1,2,3}Mapper` functions.
8
+ * Package `src/` again (— i.e., don't ignore for NPM packaging.)
8
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lionweb/ts-utils",
3
- "version": "0.7.0-beta.18",
3
+ "version": "0.7.0-beta.19",
4
4
  "description": "Utilities for LionWeb JSON",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/arrays.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Feature's values are often persisted as either `undefined`, a single object, or an array of objects,
3
+ * regardless of the actual cardinality of that feature.
4
+ * This is e.g. the case when parsing an Ecore XML metamodel file.
5
+ * This type definition captures that phenomenon.
6
+ */
7
+ export type AnyNumberOf<T> = undefined | T | T[]
8
+
9
+ /**
10
+ * Turns a {@link AnyNumberOf feature's value} into an array of objects
11
+ * (possibly empty), regardless of the feature's cardinality and how its
12
+ * value happened to be parsed.
13
+ */
14
+ export const asArray = <T>(thing: AnyNumberOf<T>): T[] => {
15
+ if (thing === undefined) {
16
+ return []
17
+ }
18
+ if (Array.isArray(thing)) {
19
+ return thing
20
+ }
21
+ return [thing]
22
+ }
23
+
24
+ /**
25
+ * @return a view of the given array of items with duplicates removed.
26
+ */
27
+ export const uniquesAmong = <T>(ts: T[]): T[] =>
28
+ [...new Set(ts)]
29
+
30
+
31
+ /**
32
+ * @return the defined values of given type (parameter) `T` from the given array,
33
+ * leaving out the `null` or `undefined` values.
34
+ */
35
+ export const keepDefineds = <T>(ts: (T | null | undefined)[]): T[] =>
36
+ ts.filter((t) => t !== undefined && t !== null) as T[]
37
+
38
+
39
+ /**
40
+ * @return the last element of the given `ts` array.
41
+ * @throws an {@link Error} if the array is empty, so it doesn't have a last element.
42
+ */
43
+ export const lastOfArray = <T>(ts: T[]): T => {
44
+ if (ts.length === 0) {
45
+ throw new Error(`empty array doesn't have a last element`)
46
+ }
47
+ return ts[ts.length - 1]
48
+ }
49
+
package/src/cycles.ts ADDED
@@ -0,0 +1,31 @@
1
+ export type NextsFunction<T> = (t: T) => T[]
2
+
3
+
4
+ /**
5
+ * Compute whether there's a cycle of dependencies, starting with `thing` and computing "nexts" with the given `nextsFunction`.
6
+ * @return An array with a cycle of "things", starting at the given `thing`.
7
+ * An array of length 0 means: the given `thing` is not part of any cycle.
8
+ */
9
+ export const cycleWith = <T>(thing: T, nextsFunction: NextsFunction<T>): T[] => {
10
+
11
+ const result = visit<T>(thing, [], nextsFunction)
12
+ return result.length > 0 && result[result.length - 1] === thing
13
+ ? result
14
+ : []
15
+ }
16
+
17
+ const visit = <T>(current: T, chain: T[], nextsFunction: NextsFunction<T>): T[] => {
18
+ if (chain.indexOf(current) > -1) {
19
+ // Add current to end, so we know what sub-array constitutes the actual cycle:
20
+ return [ ...chain, current ]
21
+ }
22
+ const extendedChain = [ ...chain, current ]
23
+ for (const dependency of nextsFunction(current)) {
24
+ const recursion = visit(dependency, extendedChain, nextsFunction)
25
+ if (recursion.length > 0) {
26
+ return recursion
27
+ }
28
+ }
29
+ return []
30
+ }
31
+
package/src/graphs.ts ADDED
@@ -0,0 +1,36 @@
1
+ // NB This is an almost identical copy of packages/core/utils/recursion.ts
2
+ // Copied to avoid package dependencies
3
+ /**
4
+ * Type def. of a generic "flatMap" function.
5
+ */
6
+ export type ResultMapper<T, R> = (t: T) => R[]
7
+
8
+ /**
9
+ * Returns a function that performs a "flatMap" on a graph that's specified as a start vertex. and a function that computes (outgoing) edges.
10
+ * The "flatMap" is performed depth-first, and doesn't loop on cycles.
11
+ *
12
+ * @param mapper The function that calculates the result values
13
+ * @param nextVertices The function that calculates the next edges to visit.
14
+ * @returns A function that takes a starting vertex and recursively calculates the result values for each vertex visited.
15
+ */
16
+ export const visitAndMap =
17
+ <T, R>(mapper: ResultMapper<T, R>, nextVertices: (t: T) => T[]): ResultMapper<T, R> =>
18
+ (startVertex: T): R[] => {
19
+ const visited: T[] = []
20
+ const results: R[] = []
21
+ const recurse = (t: T) => {
22
+ if (visited.indexOf(t) > -1) {
23
+ return
24
+ }
25
+ visited.push(t)
26
+ results.push(...mapper(t))
27
+ nextVertices(t).forEach(recurse)
28
+ }
29
+ recurse(startVertex)
30
+ return results
31
+ }
32
+
33
+ /**
34
+ * A mapper function that returns the node itself as result
35
+ */
36
+ export const selfMapper = <T>(t: T) => [t]
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export * from "./arrays.js"
2
+ export * from "./cycles.js"
3
+ export * from "./graphs.js"
4
+ export * from "./json.js"
5
+ export * from "./maps.js"
6
+ export * from "./nested-maps.js"
7
+ export * from "./recursion.js"
8
+ export * from "./string-sorting.js"
9
+ export * from "./string-mapping.js"
10
+ export * from "./toposort.js"
package/src/json.ts ADDED
@@ -0,0 +1,8 @@
1
+ /** Normalized JSON with 4 spaces as indent */
2
+ export const asPrettyJsonString = (obj: unknown): string => {
3
+ return JSON.stringify(obj, null, 4);
4
+ }
5
+ /** Minimal JSON with no spaces as indent */
6
+ export const asMinimalJsonString = (obj: unknown): string => {
7
+ return JSON.stringify(obj, null, 0);
8
+ }
package/src/maps.ts ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Computes a map id -> thing with id.
3
+ */
4
+ export const byIdMap = <T extends { id: string }>(ts: T[]): { [id: string]: T } => {
5
+ const map: { [id: string]: T } = {}
6
+ ts.forEach((t) => {
7
+ map[t.id] = t
8
+ })
9
+ return map
10
+ // TODO -> Object.fromEntries(...) but what happens then with duplicate IDs?
11
+ }
12
+
13
+
14
+ /**
15
+ * Groups a list of items according to a computed key.
16
+ */
17
+ export const groupBy = <T>(ts: T[], keyFunc: (t: T) => string): Record<string, T[]> => {
18
+ const map: Record<string, T[]> = {}
19
+ ts.forEach((t) => {
20
+ const key = keyFunc(t)
21
+ let list = map[key]
22
+ if (list === undefined) {
23
+ list = []
24
+ map[key] = list
25
+ }
26
+ list.push(t)
27
+ })
28
+ return map
29
+ }
30
+
31
+
32
+ /**
33
+ * Filters the values of a map/record according the given predicate.
34
+ */
35
+ export const filterValues = <V>(map: Record<string, V>, predicate: (v: V) => boolean): Record<string, V> =>
36
+ Object.fromEntries(
37
+ Object.entries(map)
38
+ .filter(([_, value]) => predicate(value))
39
+ )
40
+
41
+
42
+ /**
43
+ * Computes the given items whose computed keys are duplicates, as a map key &rarr; items.
44
+ */
45
+ export const duplicatesAmong = <T>(ts: T[], keyFunc: (t: T) => string): Record<string, T[]> =>
46
+ filterValues(groupBy(ts, keyFunc), (ts) => ts.length > 1)
47
+
48
+
49
+ /**
50
+ * Maps the values of a map/record according to the given mapping function.
51
+ */
52
+ export const mapValues = <V, W>(map: Record<string, V>, valFunc: (v: V) => W): Record<string, W> =>
53
+ Object.fromEntries(
54
+ Object.entries(map)
55
+ .map(([key, value]) => [key, valFunc(value)])
56
+ )
57
+
58
+
59
+ /**
60
+ * Gets the value under the given key from the given map,
61
+ * creating that value when that key wasn't present yet.
62
+ * This allows for convenient “chaining” of map-lookups,
63
+ * e.g. to build up nested maps.
64
+ */
65
+ export const lazyMapGet = <T>(map: { [key: string]: T }, key: string, createThunk: () => T): T => {
66
+ if (key in map) {
67
+ return map[key]
68
+ }
69
+ const value = createThunk()
70
+ map[key] = value
71
+ return value
72
+ }
73
+
74
+
75
+ /**
76
+ * @return a map with each key-value pair the result of mapping an item in the given list using the given key and value functions.
77
+ */
78
+ export const mapFrom = <T, V>(ts: T[], keyFunc: (t: T) => string, valueFunc: (t: T) => V): Record<string, V> =>
79
+ Object.fromEntries(
80
+ ts.map((t) => [keyFunc(t), valueFunc(t)])
81
+ )
82
+
@@ -0,0 +1,99 @@
1
+ import { groupBy } from "./maps.js"
2
+
3
+
4
+ export type Nested1Map<T> = Record<string, T> // (for conceptual continuity)
5
+ export type Nested2Map<T> = Record<string, Record<string, T>>
6
+ export type Nested3Map<T> = Record<string, Record<string, Record<string, T>>>
7
+
8
+ export type Nested1Mapper<T, R> = (nested1Map: Nested1Map<T>) => Nested1Map<R>
9
+ export const mapValuesMapper =
10
+ <T, R>(valueMapFunc: (t: T) => R): Nested1Mapper<T, R> =>
11
+ (map: Record<string, T>): Record<string, R> =>
12
+ Object.fromEntries(Object.entries(map).map(([key, value]) => [key, valueMapFunc(value)]))
13
+ // === mapValues(map, valueFunc)
14
+
15
+ export type Nested2Mapper<T, R> = (nested2Map: Nested2Map<T>) => Nested2Map<R>
16
+ export const nested2Mapper = <T, R>(valueMapFunc: (t: T) => R): Nested2Mapper<T, R> =>
17
+ mapValuesMapper(mapValuesMapper(valueMapFunc))
18
+
19
+ export type Nested3Mapper<T, R> = (nested3Map: Nested3Map<T>) => Nested3Map<R>
20
+ export const nested3Mapper = <T, R>(valueMapFunc: (t: T) => R): Nested3Mapper<T, R> =>
21
+ mapValuesMapper(nested2Mapper(valueMapFunc))
22
+
23
+ /**
24
+ * Return a function that groups an array of things using a group function as a
25
+ * map : string (group key) &rarr; array of grouped things.
26
+ */
27
+ export const grouper =
28
+ <T>(key1Func: (t: T) => string): ((ts: T[]) => Nested1Map<T[]>) =>
29
+ (ts: T[]) =>
30
+ groupBy(ts, key1Func)
31
+
32
+ /**
33
+ * Return a function that groups an array of things using two group functions as a nested map
34
+ * map : string (group key 1) &rarr;
35
+ * map : string (group key 2) &rarr; array of grouped things.
36
+ */
37
+ export const nested2Grouper =
38
+ <T>(key1Func: (t: T) => string, key2Func: (t: T) => string): ((ts: T[]) => Nested2Map<T[]>) =>
39
+ (ts: T[]) =>
40
+ mapValuesMapper(grouper(key2Func))(grouper(key1Func)(ts))
41
+ // === mapValuesMapper((vs) => groupBy(vs, key2Func))(groupBy(ts, key1Func))
42
+
43
+ /**
44
+ * Return a function that groups an array of things using two group functions as a nested map
45
+ * map : string (group key 1) &rarr;
46
+ * map : string (group key 2) &rarr;
47
+ * map : string (group key 3) &rarr; array of grouped things.
48
+ */
49
+ export const nested3Grouper =
50
+ <T>(key1Func: (t: T) => string, key2Func: (t: T) => string, key3Func: (t: T) => string): ((ts: T[]) => Nested3Map<T[]>) =>
51
+ (ts: T[]) =>
52
+ mapValuesMapper(nested2Grouper(key2Func, key3Func))(grouper(key1Func)(ts))
53
+
54
+ /**
55
+ * Flat-maps over the values of a
56
+ * map : string (group key) &rarr; values
57
+ * using the map function, which is also provided with the keys.
58
+ */
59
+ export const flatMapValues = <T, R>(map: Record<string, T>, mapFunc: (t: T, key1: string) => R): R[] =>
60
+ Object
61
+ .entries(map)
62
+ .map(
63
+ ([key1, t]) =>
64
+ mapFunc(t, key1)
65
+ )
66
+
67
+ /**
68
+ * Flat-maps over the values of a nested map
69
+ * map : string (group key 1) &rarr;
70
+ * map: string (group key 2) &rarr; values
71
+ * using the map function, which is also provided with the keys.
72
+ */
73
+ export const nestedFlatMap2 = <T, R>(nested2Map: Nested2Map<T>, map2Func: (t: T, key1: string, key2: string) => R): R[] =>
74
+ Object
75
+ .entries(nested2Map)
76
+ .flatMap(
77
+ ([key1, nestedMap1]) =>
78
+ flatMapValues(nestedMap1, (t, key2) =>
79
+ map2Func(t, key1, key2)
80
+ )
81
+ )
82
+
83
+ /**
84
+ * Flat-maps over the values of a nested map
85
+ * map : string (group key 1) &rarr;
86
+ * map: string (group key 2) &rarr;
87
+ * map: string (group key 3) &rarr; values
88
+ * using the map function, which is also provided with the keys.
89
+ */
90
+ export const nestedFlatMap3 = <T, R>(nested3Map: Nested3Map<T>, map3Func: (t: T, key1: string, key2: string, key3: string) => R): R[] =>
91
+ Object
92
+ .entries(nested3Map)
93
+ .flatMap(
94
+ ([key1, nestedMap2]) =>
95
+ nestedFlatMap2(nestedMap2, (t, key2, key3) =>
96
+ map3Func(t, key1, key2, key3)
97
+ )
98
+ )
99
+
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Type def. of a generic "flatMap" function.
3
+ */
4
+ type FlatMapper<T, R> = (t: T) => R[]
5
+
6
+ /**
7
+ * Performs a "flatMap" on a graph that's specified as a start vertex and a function that computes (outgoing) edges.
8
+ * The "flatMap" is performed depth-first, and doesn't loop on cycles.
9
+ */
10
+ const flatMapNonCyclingFollowing = <T, R>(
11
+ mapper: FlatMapper<T, R>,
12
+ nextVertices: (t: T) => T[]
13
+ ): FlatMapper<T, R> =>
14
+ (startVertex: T): R[] => {
15
+ const visited: T[] = []
16
+ const rs: R[] = []
17
+ const recurse = (t: T) => {
18
+ if (visited.indexOf(t) > -1) {
19
+ return
20
+ }
21
+ visited.push(t)
22
+ rs.push(...mapper(t))
23
+ nextVertices(t).forEach(recurse)
24
+ }
25
+ recurse(startVertex)
26
+ return rs
27
+ }
28
+
29
+
30
+ const trivialFlatMapper = <T>(t: T) => [t]
31
+
32
+
33
+ export type {
34
+ FlatMapper
35
+ }
36
+
37
+ export {
38
+ flatMapNonCyclingFollowing,
39
+ trivialFlatMapper
40
+ }
41
+
@@ -0,0 +1,38 @@
1
+ /**
2
+ * A type for functions that map any number of strings (as a variadic argument) to a single string.
3
+ */
4
+ export type StringsMapper = (...strings: string[]) => string
5
+
6
+ /**
7
+ * A type for functions that map strings to strings.
8
+ */
9
+ export type StringMapper = (str: string) => string
10
+
11
+
12
+ /**
13
+ * @return a {@link StringsMapper function} that concatenates the strings passed into it, using the given separator string.
14
+ */
15
+ export const concatenator = (separator: string): StringsMapper =>
16
+ (...strings) => strings.join(separator)
17
+
18
+ /**
19
+ * @return a {@link StringsMapper function} that picks the last string of the strings passed into it.
20
+ * (This means that there must always be at least one string passed in.)
21
+ */
22
+ export const lastOf: StringsMapper =
23
+ (...strings) => strings[strings.length - 1]
24
+
25
+ /**
26
+ * @return a {@link StringsMapper function} that chains the given {@link StringsMapper functions} (at least 1),
27
+ * in thar order.
28
+ * (This is straightforward functional composition.)
29
+ */
30
+ export const chain = (stringsMapper: StringsMapper, ...stringMappers: StringMapper[]): StringsMapper =>
31
+ (...strings: string[]) => {
32
+ let result = stringsMapper(...strings)
33
+ stringMappers.forEach((mapper) => {
34
+ result = mapper(result)
35
+ })
36
+ return result
37
+ }
38
+
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Various functional utilities for sorting things.
3
+ */
4
+
5
+
6
+ type Comparer<T> = (l: T, r: T) => number
7
+
8
+ const stringCompare: Comparer<string> = (l, r): number =>
9
+ l === r ? 0 : (l > r ? 1 : -1)
10
+
11
+ const stringyCompare = <T>(keyFunc: (t: T) => string): Comparer<T> =>
12
+ (l: T, r: T) => stringCompare(keyFunc(l), keyFunc(r))
13
+
14
+ export const sortByStringKey = <T>(ts: T[], keyFunc: (t: T) => string) =>
15
+ [...ts].sort(stringyCompare(keyFunc))
16
+
17
+
18
+ export type StringSorter = (strings: string[]) => string[]
19
+
20
+ const sortedStringsWith = (strMap: (str: string) => string): StringSorter =>
21
+ (strings) => {
22
+ strings.sort((l, r) => strMap(l).localeCompare(strMap(r)))
23
+ return strings
24
+ }
25
+
26
+ export const sortedStrings = sortedStringsWith((str) => str)
27
+
28
+ export const sortedStringsByUppercase = sortedStringsWith((str) => str.toUpperCase())
29
+
@@ -0,0 +1,46 @@
1
+ // Copyright 2025 TRUMPF Laser SE and other contributors
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License")
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+ // SPDX-FileCopyrightText: 2025 TRUMPF Laser SE and other contributors
16
+ // SPDX-License-Identifier: Apache-2.0
17
+
18
+ /**
19
+ * Computes the topological order of the transitive closure of the graph with the given vertices and edges given by the edge function,
20
+ * or returns {@code false} if there's a cycle.
21
+ */
22
+ export const dependencyOrderOf = <T>(vertices: T[], edgesOf: (vertex: T) => T[]): T[] | false => {
23
+ const ordered: T[] = []
24
+
25
+ const visit = (current: T, chain: T[]) => {
26
+ if (ordered.indexOf(current) > -1) {
27
+ return false
28
+ }
29
+ if (chain.indexOf(current) > -1) {
30
+ return true
31
+ }
32
+ const extendedChain = [ ...chain, current ]
33
+ const hasCycle = edgesOf(current).some(
34
+ (edge) => visit(edge, extendedChain)
35
+ )
36
+ ordered.push(current)
37
+ return hasCycle
38
+ }
39
+
40
+ const hasCycle = vertices.some(
41
+ (vertex) => visit(vertex, [])
42
+ )
43
+
44
+ return hasCycle ? false : ordered
45
+ }
46
+