Skip to content

Commit b62fa95

Browse files
committed
#195 - support for metadata value matchers
- mdata value matchers for boolean (valueB) and string (value, valueS, valueD, valueE, valueED) fully covered by unit tests and bugs fixed
1 parent 7fe8355 commit b62fa95

File tree

2 files changed

+495
-80
lines changed

2 files changed

+495
-80
lines changed

src/custom-sort/mdata-matchers.ts

Lines changed: 170 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,104 @@ import {
33
} from "./matchers";
44
import {NormalizerFn} from "./custom-sort-types";
55
import {CollatorCompare, CollatorTrueAlphabeticalCompare} from "./custom-sort";
6+
import {
7+
SNB,
8+
MDVConverter,
9+
SpecValueConverter,
10+
ValueConverters
11+
} from "./value-converters";
612

13+
type MDataValueType = string|number|boolean|Array<any>
714
export interface MDataMatcher {
8-
(mdataValue: string): boolean
15+
(mdataValue: MDataValueType|undefined): boolean
916
}
1017

11-
export type SN = string|number
12-
export type SNResult<T extends SN> = T extends number ? number : string
13-
export type CompareFn<T extends SN> = (a: T, b: T) => number
14-
export interface MDataMatcherFactory<T extends SN> {
15-
(specsMatch: string|RegExpMatchArray, compareFn: CompareFn<SNResult<T>>, mdvConverter: MDVConverter<SNResult<T>>): MDataMatcher
18+
export type SorNorB<T = SNB> = T extends number ? number : (T extends string ? string : boolean)
19+
export type CompareFn<T = SNB> = (a: T, b: T) => number
20+
21+
export interface MDataMatcherFactory<T extends SNB> {
22+
(specsMatch: string|RegExpMatchArray,
23+
compareFn: CompareFn<SorNorB<T>>,
24+
//mdvConverter: MDVConverter<SorNorB<T>>,
25+
//typeRepresentative: SorNorB<T>
26+
): MDataMatcher|undefined
1627
}
1728

18-
interface ValueMatcherSpec<T extends SN> {
29+
interface ValueMatcherSpec<T extends SNB> {
1930
specPattern: string|RegExp,
20-
valueMatcherFnFactory: MDataMatcherFactory<SNResult<T>>
21-
compareFn: CompareFn<SNResult<T>>
22-
mdvConterter?: MDVConverter<SNResult<T>>
31+
valueMatcherFnFactory: MDataMatcherFactory<SorNorB<T>>
32+
compareFn: CompareFn<SorNorB<T>>
33+
//mdvConterter: MDVConverter<SorNorB<T>>
2334
unitTestsId: string
2435
}
2536

37+
// Syntax sugar to enforce TS type checking on matchers configurations
38+
function newStingValueMatcherSpec(vc: ValueConverters, unitTestId: string, regex: RegExp, trueAlphabetical?: boolean): ValueMatcherSpec<string> {
39+
return {
40+
specPattern: regex,
41+
valueMatcherFnFactory: getPlainValueMatcherFnFactory<string>(vc, vc.specToStringConverter.bind(vc), '' /* type representative */),
42+
compareFn: trueAlphabetical ? CollatorTrueAlphabeticalCompare : CollatorCompare,
43+
unitTestsId: unitTestId
44+
}
45+
}
46+
function newNumberValueMatcherSpec(vc: ValueConverters, unitTestId: string, regex: RegExp, representative: number): ValueMatcherSpec<number> {
47+
return {
48+
specPattern: regex,
49+
valueMatcherFnFactory: getPlainValueMatcherFnFactory<number>(
50+
vc,
51+
(representative == ~~representative) ? vc.specToIntConverter.bind(vc) : vc.specToFloatConverter.bind(vc),
52+
representative
53+
),
54+
compareFn: (representative == ~~representative) ? CompareIntFn : CompareFloatFn,
55+
unitTestsId: unitTestId
56+
}
57+
}
58+
59+
function newBooleanValueMatcherSpec(vc: ValueConverters, unitTestId: string, regex: RegExp): ValueMatcherSpec<boolean> {
60+
return {
61+
specPattern: regex,
62+
valueMatcherFnFactory: getPlainValueMatcherFnFactory<boolean>(vc, vc.specToBooleanConverter.bind(vc), true /* type representative */),
63+
compareFn: CompareBoolFn,
64+
unitTestsId: unitTestId
65+
}
66+
}
67+
2668
export interface MDataMatcherParseResult {
2769
m: MDataMatcher
2870
remainder: string
2971
}
3072

31-
const VALUE_MATCHER_REGEX = /value\(([^)]+)\)/ // 001 === 1
32-
const VALUE_TRUE_ALPHABETIC_MATCHER_REGEX = /valueE\(([^)]+)\)/ // 001 != 1
33-
function getPlainValueMatcherFn(specsMatch: RegExpMatchArray, compareFn: CompareFn<string>) {
34-
const EXACT_VALUE_IDX = 1 // Related to the spec regexp
35-
const expectedValue = specsMatch[EXACT_VALUE_IDX].trim()
36-
return (mdataValue: string): boolean => {
37-
return compareFn(mdataValue, expectedValue) === 0
73+
const VALUE_MATCHER_REGEX = /value\(([^)]*)\)/ // 001 === 1
74+
const STR_VALUE_MATCHER_REGEX = /valueS\(([^)]*)\)/ // 001 === 1
75+
const VALUE_MATCHER_WITH_DEFAULT_REGEX = /valueD\(([^:]*):([^)]+)\)/ // 001 === 1
76+
const VALUE_TRUE_ALPHABETIC_MATCHER_REGEX = /valueE\(([^)]*)\)/ // 001 != 1
77+
const VALUE_TRUE_ALPHABETIC_MATCHER_WITH_DEFAULT_REGEX = /valueED\(([^:]*):([^)]*)\)/ // 001 != 1
78+
79+
const INT_VALUE_MATCHER_REGEX = /valueN\((\s*([-+]?\d+(?:E[-+]?\d+)?)\s*)\)/i
80+
const FLOAT_VALUE_MATCHER_REGEX = /valueF\(\s*([-+]?\d+\.\d+(?:E[-+]?\d+)?)\s*\)/i
81+
const BOOL_VALUE_MATCHER_REGEX = /valueB\(\s*(true|false|yes|no|\d)\s*\)/i // for \d only 0 or 1 are accepted, intentionally \d spec here
82+
83+
function getPlainValueMatcherFnFactory<T extends SNB>(vc: ValueConverters, specValueConverter: SpecValueConverter<SorNorB<T>>, theType: any): MDataMatcherFactory<T> {
84+
return (specsMatch: RegExpMatchArray, compareFn: CompareFn<SorNorB<T>>): MDataMatcher|undefined => {
85+
const EXACT_VALUE_IDX = 1 // Related to the spec regexp
86+
const DEFAULT_MDATA_VALUE_FOR_EMPTY_VALUE_IDX = 2 // Related to the spec regexp
87+
const expectedValueString: string|undefined = specsMatch[EXACT_VALUE_IDX] // Intentionally not trimming here - string matchers support spaces
88+
const expectedValue: SorNorB<T>|undefined = specValueConverter(expectedValueString)
89+
if (expectedValue===undefined) {
90+
return undefined // syntax error in expected value in spec
91+
}
92+
let mdvConverter: MDVConverter<SorNorB<T>>|undefined = vc.getMdvConverters()[typeof theType]
93+
if (mdvConverter === undefined) {
94+
return undefined // Error in the code, theType should be one of the supported types
95+
}
96+
return (mdataValue: MDataValueType | undefined): boolean => {
97+
const mdvToUse = mdataValue !== undefined ? mdataValue : specsMatch[DEFAULT_MDATA_VALUE_FOR_EMPTY_VALUE_IDX]?.trim()
98+
const mdv = mdvConverter(mdvToUse)
99+
if (mdv === undefined) {
100+
return false // empty metadata value does not match any expected value
101+
}
102+
return compareFn(mdv, expectedValue) === 0
103+
}
38104
}
39105
}
40106

@@ -51,30 +117,31 @@ const RANGE_NUMERIC_MATCHER_REGEX_FLOAT = /rangeF([[(])\s*?(-?\d+\.\d+)?\s*,\s*(
51117

52118
const CompareIntFn: CompareFn<number> = (a: number, b: number) => a - b
53119
const CompareFloatFn: CompareFn<number> = (a: number, b: number) => a - b
54-
type MDVConverter<T extends SN> = (s: string) => SNResult<T>
120+
const CompareBoolFn: CompareFn<boolean> = (a: boolean, b: boolean) => a === b ? 0 : (a ? 1 : -1)
55121

122+
/*
56123
enum RangeEdgeType { INCLUSIVE, EXCLUSIVE}
57-
function getRangeMatcherFn<T extends SN>(specsMatch: RegExpMatchArray, compareFn: CompareFn<SNResult<T>>, mdvConverter: MDVConverter<T>) {
124+
function getRangeMatcherFn<T extends SN>(specsMatch: RegExpMatchArray, compareFn: CompareFn<SorN<T>>, mdvConverter: MDVConverter<SorN<T>>) {
58125
const RANGE_START_TYPE_IDX = 1
59126
const RANGE_START_IDX = 2
60127
const RANGE_END_IDX = 3
61128
const RANGE_END_TYPE_IDX = 4
62129
const rangeStartType: RangeEdgeType = specsMatch[RANGE_START_TYPE_IDX] === '(' ? RangeEdgeType.EXCLUSIVE : RangeEdgeType.INCLUSIVE
63-
const rangeStartValue: SNResult<T> = mdvConverter(specsMatch[RANGE_START_IDX].trim())
64-
const rangeEndValue: SNResult<T> = mdvConverter(specsMatch[RANGE_END_IDX].trim())
130+
const rangeStartValue: SorN<T>|undefined = mdvConverter(specsMatch[RANGE_START_IDX]?.trim())
131+
const rangeEndValue: SorN<T>|undefined = mdvConverter(specsMatch[RANGE_END_IDX]?.trim())
65132
const rangeEndType: RangeEdgeType = specsMatch[RANGE_END_TYPE_IDX] === ')' ? RangeEdgeType.EXCLUSIVE : RangeEdgeType.INCLUSIVE
66-
return (mdataValue: string): boolean => {
67-
let rangeStartMatched = true
68-
const mdv: SNResult<T> = mdvConverter(mdataValue)
69-
if (rangeStartValue) {
133+
return (mdataValue: string|undefined): boolean => {
134+
const mdv: SorN<T>|undefined = mdvConverter(mdataValue?.trim())
135+
let rangeStartMatched = mdv!==undefined
136+
if (mdv!==undefined && rangeStartValue!==undefined) { // rangeStartValue can be '0' or numeric 0
70137
if (rangeStartType === RangeEdgeType.INCLUSIVE) {
71138
rangeStartMatched = compareFn (mdv, rangeStartValue) >= 0
72139
} else {
73140
rangeStartMatched = compareFn (mdv, rangeStartValue) > 0
74141
}
75142
}
76-
let rangeEndMatched = true
77-
if (rangeEndValue) {
143+
let rangeEndMatched = mdv!==undefined
144+
if (mdv!==undefined && rangeEndValue!==undefined) { // rangeStartValue can be '0' or numeric 0
78145
if (rangeEndType === RangeEdgeType.INCLUSIVE) {
79146
rangeEndMatched = compareFn (mdv, rangeEndValue) <= 0
80147
} else {
@@ -85,73 +152,97 @@ function getRangeMatcherFn<T extends SN>(specsMatch: RegExpMatchArray, compareFn
85152
return rangeStartMatched && rangeEndMatched
86153
}
87154
}
155+
*/
88156

89-
const ValueMatchers: ValueMatcherSpec<SN>[] = [
90-
{ specPattern: VALUE_MATCHER_REGEX,
91-
valueMatcherFnFactory: getPlainValueMatcherFn,
92-
compareFn: CollatorCompare,
93-
unitTestsId: 'value'
94-
}, {
95-
specPattern: VALUE_TRUE_ALPHABETIC_MATCHER_REGEX,
96-
valueMatcherFnFactory: getPlainValueMatcherFn,
97-
compareFn: CollatorTrueAlphabeticalCompare,
98-
unitTestsId: 'valueE'
99-
}, {
100-
specPattern: RANGE_MATCHER_REGEX,
101-
valueMatcherFnFactory: getRangeMatcherFn,
102-
compareFn: CollatorCompare,
103-
unitTestsId: 'range'
104-
},{
105-
specPattern: RANGE_TRUE_ALPHABETIC_MATCHER_REGEX,
106-
valueMatcherFnFactory: getRangeMatcherFn,
107-
compareFn: CollatorTrueAlphabeticalCompare,
108-
unitTestsId: 'rangeE'
109-
},{
110-
specPattern: RANGE_NUMERIC_MATCHER_REGEX_INT,
111-
valueMatcherFnFactory: getRangeMatcherFn,
112-
compareFn: CompareIntFn,
113-
mdvConterter: (s: string) => ~~s,
114-
unitTestsId: 'rangeN'
115-
},{
116-
specPattern: RANGE_NUMERIC_MATCHER_REGEX_FLOAT,
117-
valueMatcherFnFactory: getRangeMatcherFn,
118-
compareFn: CompareFloatFn,
119-
mdvConterter: (s: string) => parseFloat(s),
120-
unitTestsId: 'rangeF'
121-
}, {
122-
specPattern: 'any-value', // Artificially added for testing purposes
123-
valueMatcherFnFactory: () => (s: string) => true,
124-
compareFn: CollatorCompare, // Not used
125-
unitTestsId: 'any-value-explicit'
126-
}
127-
]
157+
let valueMatchersCache: ValueMatcherSpec<SNB>[]|undefined = undefined
158+
159+
const valueConverters = new ValueConverters()
160+
161+
// Dependency injection of valueConverters for unit testing purposes
162+
function getValueMatchers(vc?: ValueConverters) {
163+
return valueMatchersCache ??= [
164+
newStingValueMatcherSpec(vc ?? valueConverters, 'value', VALUE_MATCHER_REGEX),
165+
newStingValueMatcherSpec(vc ?? valueConverters, 'valueS', STR_VALUE_MATCHER_REGEX),
166+
newStingValueMatcherSpec(vc ?? valueConverters, 'valueD', VALUE_MATCHER_WITH_DEFAULT_REGEX),
167+
newStingValueMatcherSpec(vc ?? valueConverters, 'valueE', VALUE_TRUE_ALPHABETIC_MATCHER_REGEX, true),
168+
newStingValueMatcherSpec(vc ?? valueConverters, 'valueED', VALUE_TRUE_ALPHABETIC_MATCHER_WITH_DEFAULT_REGEX, true),
169+
newNumberValueMatcherSpec(vc ?? valueConverters, 'valueN', INT_VALUE_MATCHER_REGEX, 1 /* type representative */),
170+
newNumberValueMatcherSpec(vc ?? valueConverters, 'valueF', FLOAT_VALUE_MATCHER_REGEX, 1.1 /* type representative */),
171+
newBooleanValueMatcherSpec(vc ?? valueConverters, 'valueB', BOOL_VALUE_MATCHER_REGEX),
172+
/*
173+
174+
// Range matchers
175+
{
176+
specPattern: RANGE_MATCHER_REGEX,
177+
valueMatcherFnFactory: getRangeMatcherFn,
178+
compareFn: CollatorCompare,
179+
unitTestsId: 'range'
180+
},{
181+
specPattern: RANGE_TRUE_ALPHABETIC_MATCHER_REGEX,
182+
valueMatcherFnFactory: getRangeMatcherFn,
183+
compareFn: CollatorTrueAlphabeticalCompare,
184+
unitTestsId: 'rangeE'
185+
},{
186+
specPattern: RANGE_NUMERIC_MATCHER_REGEX_INT,
187+
valueMatcherFnFactory: getRangeMatcherFn,
188+
compareFn: CompareIntFn,
189+
mdvConterter:
190+
unitTestsId: 'rangeN'
191+
},{
192+
specPattern: RANGE_NUMERIC_MATCHER_REGEX_FLOAT,
193+
valueMatcherFnFactory: getRangeMatcherFn,
194+
compareFn: CompareFloatFn,
195+
mdvConterter:
196+
unitTestsId: 'rangeF'
197+
},*/ {
198+
specPattern: 'any-value', // Artificially added for testing purposes
199+
valueMatcherFnFactory: () => (s: any) => true,
200+
compareFn: (a, b) => 0, // Not used
201+
unitTestsId: 'any-value-explicit'
202+
}
203+
]
204+
}
128205

129206
export const tryParseAsMDataMatcherSpec = (s: string): MDataMatcherParseResult|undefined => {
130207
// Simplistic initial implementation of the idea, not closing the way to more complex implementations
131-
for (const matcherSpec of ValueMatchers) {
208+
for (const matcherSpec of getValueMatchers()) {
132209
if ('string' === typeof matcherSpec.specPattern && s.trim().startsWith(matcherSpec.specPattern)) {
133-
return {
134-
m: matcherSpec.valueMatcherFnFactory(matcherSpec.specPattern, matcherSpec.compareFn, (s: string) => s ),
210+
const mdMatcher: MDataMatcher|undefined = matcherSpec.valueMatcherFnFactory(matcherSpec.specPattern, matcherSpec.compareFn)
211+
return mdMatcher ? {
212+
m: mdMatcher,
135213
remainder: s.substring(matcherSpec.specPattern.length).trim()
136-
}
214+
} : undefined
137215
} else { // regexp
138216
const match = s.match(matcherSpec.specPattern)
139217
if (match) {
140-
return {
141-
m: matcherSpec.valueMatcherFnFactory(match, matcherSpec.compareFn, matcherSpec.mdvConterter ?? (s => s)),
218+
const mdMatcher: MDataMatcher|undefined = matcherSpec.valueMatcherFnFactory(match, matcherSpec.compareFn)
219+
return mdMatcher ? {
220+
m: mdMatcher,
142221
remainder: s.substring(match[0].length).trim()
143-
}
222+
} : undefined
144223
}
145224
}
146225
}
147226
return undefined
148227
}
149228

150229
export const _unitTests = {
151-
matcherFn_value: ValueMatchers.find((it) => it.unitTestsId === 'value'),
152-
matcherFn_range: ValueMatchers.find((it) => it.unitTestsId === 'range'),
153-
matcherFn_rangeE: ValueMatchers.find((it) => it.unitTestsId === 'rangeE'),
154-
matcherFn_rangeN: ValueMatchers.find((it) => it.unitTestsId === 'rangeN'),
155-
matcherFn_rangeF: ValueMatchers.find((it) => it.unitTestsId === 'rangeF'),
156-
matcherFn_anyValue: ValueMatchers.find((it) => it.unitTestsId === 'any-value-explicit'),
230+
getMatchers(vc: ValueConverters) {
231+
const valueMatchers = getValueMatchers(vc)
232+
return {
233+
matcherFn_value: valueMatchers.find((it) => it.unitTestsId === 'value'),
234+
matcherFn_valueS: valueMatchers.find((it) => it.unitTestsId === 'valueS'),
235+
matcherFn_valueD: valueMatchers.find((it) => it.unitTestsId === 'valueD'),
236+
matcherFn_valueE: valueMatchers.find((it) => it.unitTestsId === 'valueE'),
237+
matcherFn_valueED: valueMatchers.find((it) => it.unitTestsId === 'valueED'),
238+
matcherFn_valueN: valueMatchers.find((it) => it.unitTestsId === 'valueN'),
239+
matcherFn_valueF: valueMatchers.find((it) => it.unitTestsId === 'valueF'),
240+
matcherFn_valueB: valueMatchers.find((it) => it.unitTestsId === 'valueB'),
241+
matcherFn_range: valueMatchers.find((it) => it.unitTestsId === 'range'),
242+
matcherFn_rangeE: valueMatchers.find((it) => it.unitTestsId === 'rangeE'),
243+
matcherFn_rangeN: valueMatchers.find((it) => it.unitTestsId === 'rangeN'),
244+
matcherFn_rangeF: valueMatchers.find((it) => it.unitTestsId === 'rangeF'),
245+
matcherFn_anyValue: valueMatchers.find((it) => it.unitTestsId === 'any-value-explicit'),
246+
}
247+
}
157248
}

0 commit comments

Comments
 (0)