/* Copyright (c) 2020 Richard Rodger, MIT License */ /* $lab:coverage:off$ */ import { Gex } from 'gex' /* $lab:coverage:on$ */ // NOTE: naming convention: // key : pattern element key name string // fix : pattern element match value // val : potential match value export interface Matcher { make(key: string, fix: any): MatchValue | undefined scan(mvs: MatchValue[], opts?: any): ScanResult } export interface ScanResult { complete: boolean, sound: boolean, gaps: any[], overs: any[], why?: string } export interface MatchValue { match(val: any): boolean same(mv: MatchValue | undefined): boolean kind: string fix: any meta: any keymap?: any } export class GexMatcher implements Matcher { constructor() { } make(key: string, fix: any) { if ('string' === typeof fix && fix.match(/[*?]/)) { let gex = Gex(fix) return { kind: 'gex', match: (val: any) => null != gex.on(val), fix: fix, meta: {}, same(mv: MatchValue) { return null != mv && mv.kind === this.kind && mv.fix === this.fix } } } else return undefined } // TODO: pretty primitive, just looks for '*' scan(mvs: MatchValue[], opts?: any): ScanResult { let has_match_any = mvs.filter(mv => '*' === mv.fix).length > 0 return { complete: has_match_any, sound: has_match_any, gaps: [], overs: [], why: 'no-star' } } } // TODO: space joins with & // TODO: recongnize 'and' 'or' // TODO: integers: <1, >1&<2, >2 is complete // TODO: any: * is -Inf>=&&<=+Inf \ intervals - ie. gaps // TODO: non-Number types: special case // NOTE: '/' == '\\' const IntervalRE = new RegExp([ '^/s*', // optional whitespace '(=*[<>/(/[]?=*)?' + // 1, lenient operator symbol '/s*' + // optional whitespace '([-+0-9a-fA-FeEoOxX]+(/.([0-9a-fA-FeEoOxX]+))?)' + // 2,3,4 number '([/)/]]?)' + // 5, optional interval operator symbol '(' + // 6, start optional second term '/s*(,|&+|/|+|/./.)' + // 7, join '/s*' + // optional whitespace '(=*[<>]?=*)' + // 8, lenient operator symbol '/s*' + // optional whitespace '([-+.0-9a-fA-FeEoOxX]+)' + // 9, number '/s*' + // optional whitespace '([/)/]]?)' + // 10, interval operator symbol ')?' + // end optional second term '/s*$', // optional whitespace ].join('').replace(/\//g, '\\')) export class IntervalMatcher implements Matcher { kind = 'interval' constructor() { } // == sames as =, <= same as =< static normop = (op: string) => null == op ? null : (((op.match(/([<>\(\)\[\]])/) || [])[1] || '') + ((op.match(/(=)/) || [])[1] || '')) #and = (lhs: any, rhs?: any) => function and(x: number) { return lhs(x) && rhs(x) } #or = (lhs: any, rhs?: any) => function or(x: number) { return lhs(x) || rhs(x) } #nil = (n: any) => function nil(x: any) { return false } #err = (n: any) => function err(x: any) { return false } #mgt = (n: any) => function gt(x: any) { return x > n } #mgte = (n: any) => function gte(x: any) { return x >= n } #mlt = (n: any) => function lt(x: any) { return x < n } #mlte = (n: any) => function lte(x: any) { return x <= n } #meq = (n: any) => function eq(x: any) { return x === n } make(key: string, fix: any) { if ('string' === typeof fix && // At least one interval operator is required. // Exact numbers must be specified as '=X' fix.match(/[=<>.[()\]]/)) { let m = fix.match(IntervalRE) let meta = { jo: 'and', o0: 'err', n0: NaN, o1: 'err', n1: NaN } let match = (val: any) => false if (null != m) { let os0 = IntervalMatcher.normop(m[1]) || IntervalMatcher.normop(m[5]) let os1 = IntervalMatcher.normop(m[8]) || IntervalMatcher.normop(m[10]) let o0 = '=' === os0 ? this.#meq : '<' === os0 ? this.#mlt : ')' === os0 ? this.#mlt : '<=' === os0 ? this.#mlte : ']' === os0 ? this.#mlte : '>' === os0 ? this.#mgt : '(' === os0 ? this.#mgt : '>=' === os0 ? this.#mgte : '[' === os0 ? this.#mgte : this.#err let n0 = Number(m[2]) let n1 = null == m[9] ? NaN : Number(m[9]) let jos = m[7] let jo = null == jos ? this.#or : '&' === jos.substring(0, 1) ? this.#and : ',' === jos.substring(0, 1) ? this.#and : this.#or if ('..' === jos) { jo = this.#and o0 = this.#err === o0 ? this.#mgte : o0 os1 = '' === os1 ? '<=' : os1 } let o1 = null == os1 ? this.#nil : '=' === os1 ? this.#meq : '<' === os1 ? this.#mlt : ')' === os1 ? this.#mlt : '<=' === os1 ? this.#mlte : ']' === os1 ? this.#mlte : '>' === os1 ? this.#mgt : '>=' === os1 ? this.#mgte : this.#err // merge ops if same number if (n0 === n1) { if ('=' === os0 && null != os1) { n1 = NaN o1 = this.#nil if (os1.includes('<')) { o0 = this.#mlte } else if (os1.includes('>')) { o0 = this.#mgte } else if (os1.includes('=')) { o0 = this.#meq } else { o0 = this.#err } } else if ('=' === os1 && null != os0) { n1 = NaN o1 = this.#nil if (os0.includes('<')) { o0 = this.#mlte } else if (os0.includes('>')) { o0 = this.#mgte } else { o0 = this.#err } } } // console.log(jo(o0(n0), o1(n1)), o0(n0), o1(n1)) // if one sided interval, add the other side out to infinity if (this.#err !== o0) { if (this.#nil === o1) { if (this.#mlt === o0 || this.#mlte === o0) { o1 = o0 n1 = n0 o0 = this.#mgte n0 = Number.NEGATIVE_INFINITY jo = this.#and } else if (this.#mgt === o0 || this.#mgte === o0) { o1 = this.#mlte n1 = Number.POSITIVE_INFINITY jo = this.#and } // else this.meq ok as is } } // lower bound is always first so that interval sorting will work if (!isNaN(n1) && n1 < n0) { let to = o1 let tn = n1 n1 = n0 n0 = tn // sensible heuristic: 20..10 means >=10&<=20 if ('..' !== jos) { o1 = o0 o0 = to } } let o0f = o0(n0) let o1f = o1(n1) let check = jo(o0f, o1f) meta = { jo: check.name, o0: o0f.name, n0, o1: o1f.name, n1 } match = (val: any) => { let res = false let n = parseFloat(val) if (!isNaN(n)) { res = check(n) } return res } return { kind: 'interval', fix, meta, match, same(mv: MatchValue) { return null != mv && mv.kind === this.kind && mv.meta.jo === this.meta.jo && mv.meta.o0 === this.meta.o0 && mv.meta.n0 === this.meta.n0 && mv.meta.o1 === this.meta.o1 && mv.meta.n1 === this.meta.n1 } } } } } scan(mvs: MatchValue[], opts?: any): ScanResult { let scanres = { complete: false, sound: false, gaps: [] as any[], overs: [] as any[], lower: null, upper: null, } let bottom = Number.NEGATIVE_INFINITY let top = Number.POSITIVE_INFINITY let half_intervals: any[] = this.half_intervals(mvs) // console.log('H', half_intervals) half_intervals .reduce((c, h) => { // c: accumulated state // h: current half interval // console.log('\n\nRED') // console.dir(c, { depth: null }) // console.dir(h, { depth: null }) let heq = 'eq' === h.o let hlt = 'lt' === h.o let hlte = 'lte' === h.o let hgt = 'gt' === h.o let hgte = 'gte' === h.o let hn = h.n // NOTE: { == (or[or none if (null == c.lower) { let b = { n: bottom, o: 'gte' } c.lower = b c.upper = h if (!(bottom == hn && hgte)) { if (hgt || hgte) { // {-oo,hn} c.gaps.push([b, { n: hn, o: hgt ? 'lte' : 'lt', m: 0 }]) } else if (heq) { // {-oo,hn) c.gaps.push([b, { n: hn, o: 'lte', m: 1 }]) } } } else { let ueq = 'eq' === c.upper.o let ult = 'lt' === c.upper.o let ulte = 'lte' === c.upper.o let ugt = 'gt' === c.upper.o let ugte = 'hgte' === c.upper.o let un = c.upper.n let u = c.upper if (hn === un) { // un)[hn} OR {un](hn if ((ult && (hgte || heq)) || ((ulte || ueq) && hgt)) { //c.upper = h } else if (ueq || ult || ulte) { // {un,hn} c.gaps.push([ { n: un, o: (ueq || ulte) ? 'gt' : 'gte', m: 2, d: { u, h } }, { n: hn, o: (heq || hgte) ? 'lt' : 'lte', m: 3 } ]) } else { // TODO overlaps // console.log('OL-a', c, h) } } else if (un < hn) { if (hlt || hlte) { // console.log('OL-b', c, h) /* // overlap matches boundaries of c.upper.n..h.n c.overs.push([ { n: un, o: c.upper.o, m: 8 }, { n: hn, o: h.o, m: 9 } ]) */ //c.upper = h } else if (ueq || ult || ulte) { // {un,hn} c.gaps.push([ { n: un, o: (ueq || ulte) ? 'gt' : 'gte', m: 4 }, { n: hn, o: (heq || hgte) ? 'lt' : 'lte', m: 5 } ]) } } // hn < un else { // console.log('hn < un', hn, un) c.overs.push([ { n: hn, o: (heq || hgte) ? 'gte' : 'gt', m: 10 }, { n: un, o: (ueq || ulte) ? 'lte' : 'lt', m: 11 }, ]) } c.upper = h } return c }, scanres) let last = 0 < half_intervals.length && half_intervals[half_intervals.length - 1] // {n,+oo] if (last && top !== last.n && last.o !== 'gt' && last.o !== 'gte') { scanres.gaps.push([ { n: last.n, o: (last.o === 'eq' || last.o === 'lte') ? 'gt' : 'gte', m: 6 }, { n: top, o: 'lte', m: 7 } ]) } scanres.complete = 0 === scanres.gaps.length scanres.sound = 0 === scanres.overs.length return scanres } // NOTE: assumes n0<=n1 half_intervals(mvs: MatchValue[]) { let half_intervals: any[] = [] for (let mv of mvs) { half_intervals.push( [{ n: mv.meta.n0, o: mv.meta.o0 }, { n: mv.meta.n1, o: mv.meta.o1 }] ) } // canonical ordering of operations var os = ['lt', 'lte', 'eq', 'gte', 'gt'] var hi = half_intervals .map(hh => [ (isNaN(hh[0].n) || null == hh[0].n) ? null : hh[0], (isNaN(hh[1].n) || null == hh[1].n) ? null : hh[1]] .filter(h => null != h)) // sorting on intervals, *not* half intervals .sort((a, b) => { // sort by first term numerically if (a[0].n < b[0].n) { return -1 } else if (b[0].n < a[0].n) { return 1 } else { // sort by first term operationally var a0i = os.indexOf(a[0].o) var b0i = os.indexOf(b[0].o) if (a0i < b0i) { return -1 } else if (b0i < a0i) { return 1 } else { // sort by second term numerically if (a[1].n < b[1].n) { return -1 } else if (b[1].n < a[1].n) { return 1 } else { // sort by second term operationally var a1i = os.indexOf(a[1].o) var b1i = os.indexOf(b[1].o) return a1i < b1i ? -1 : b1i < a1i ? 1 : 0 } } } }) .reduce((hv, hh) => hv.concat(...hh), []) // console.log(hi) return hi } }