From f680b4f34826a404009a493323975cf67391da0f Mon Sep 17 00:00:00 2001 From: Nikolaus Piccolotto Date: Fri, 6 Jan 2017 17:00:47 +0100 Subject: [PATCH 1/7] :construction: #12 save progress for maybe --- lib/regex.js | 88 ++++++++++++++++++++++++++++++++------- lib/spec/nilable.js | 2 +- lib/util.js | 3 +- test/regex/alt.test.js | 20 +++++++++ test/regex/cat.test.js | 26 +++++++++++- test/regex/maybe.test.js | 41 ++++++++++++++++++ test/spec/nilable.test.js | 10 ++--- 7 files changed, 166 insertions(+), 24 deletions(-) create mode 100644 test/regex/maybe.test.js diff --git a/lib/regex.js b/lib/regex.js index 71aa873..8bcbc43 100644 --- a/lib/regex.js +++ b/lib/regex.js @@ -1,6 +1,7 @@ // http://www.ccs.neu.edu/home/turon/re-deriv.pdf import { dt, symbolToString, zip, valid, explain, getName } from './util' +import { Nilable } from './spec/nilable' import { invalid } from './symbols' import * as p from './predicates' // ops @@ -38,15 +39,32 @@ export function isRegex(x) { * ∂a (r · s) = ∂a r · s + ν(r) · ∂a s */ function catDeriv(regex, x) { - const [r, ...s] = regex.ps + const [p0, ...prest] = regex.ps + const [k0, ...krest] = regex.ks + + const derivations = [ + pcat({ + ps: [deriv(p0, x), ...prest], + ks: regex.ks, + ret: regex.ret + }) + ] + + if (acceptsNil(p0)) { + derivations.push( + deriv( + pcat({ + ps: prest, + ks: krest, + ret: [...regex.ret, k0 ? { + [k0]: null + } : null] + }))) + } else { + } + return palt({ - ps: [ - pcat({ - ps: [deriv(r, x), ...s], - ks: regex.ks, - ret: regex.ret - }) - ] + ps: derivations }) } @@ -87,10 +105,22 @@ export function deriv(regex, x) { return altDeriv(regex, x) case cat: return catDeriv(regex, x) + } + throw new Error(`Cannot derive unknown operation "${symbolToString(op)}"`) +} - default: - throw new Error(`Cannot derive unknown operation "${symbolToString(op)}"`) +function getReturn(regex) { + switch (regex.op) { + case cat: + const catProcessed = regex.ret.reduce((agg, val) => Object.assign(agg, val), {}) + // unprocessed values + regex.ks.forEach(k => catProcessed[k] = null) + return catProcessed + case acc: + case alt: + return regex.ret } + throw new Error(`Return for ${symbolToString(regex.op)} not implemented`) } /** @@ -106,12 +136,14 @@ export function regexConform(regex, data) { if (xrest.length > 0) { return regexConform(dx, xrest) } + if (acceptsNil(dx)) { + return getReturn(dx) + } return invalid } export function regexExplain(regex, path, via, value) { // no value provided, so no problems can occur - // if (p.nil(value)) { return null } @@ -158,6 +190,21 @@ export function regexExplain(regex, path, via, value) { } } +function acceptsNil(regex) { + if (!isRegex(regex)) { + return regex === p.nil || regex instanceof Nilable + } + switch (regex.op) { + case alt: + return regex.ps.some(p => acceptsNil(p)) + case cat: + return regex.ps.every(p => acceptsNil(p)) + default: + // TODO + return false + } +} + /** * Returns regex object for cat. Do not use directly, use catImpl. */ @@ -178,9 +225,10 @@ function pcat(opts = {}) { } // there are no more values // we convert the array of matches to a single map and return that - return accept(pret.reduce((agg, val) => Object.assign(agg, val), {})) + return accept(getReturn(pcat({ + ret: pret + }))) } - return { op: cat, ps, @@ -219,7 +267,6 @@ function palt(opts = {}) { ks, ret } - // if any of the alternatives is accepted, return that const acceptIdx = ps.findIndex(p => accepted(p)) if (acceptIdx !== -1) { @@ -275,8 +322,17 @@ export function plusImpl() { } // x? -export function maybeImpl() { - +export function maybeImpl(name, predicate) { + if (p.nil(name) || !p.string(name)) { + throw new Error(`Must provide a name to maybe.`) + } + if (p.nil(predicate)) { + throw new Error(`Must provide a predicate to maybe.`) + } + return palt({ + ps: [predicate, p.nil], + ks: [name, name] + }) } //???? diff --git a/lib/spec/nilable.js b/lib/spec/nilable.js index 4fc64ea..c32a217 100644 --- a/lib/spec/nilable.js +++ b/lib/spec/nilable.js @@ -6,7 +6,7 @@ import { invalid } from '../symbols' export class Nilable extends Spec { conform(value) { if (p.nil(value)) { - return value + return null } else { return dt(this.options.spec, value) } diff --git a/lib/util.js b/lib/util.js index 7960ae9..ea4996d 100644 --- a/lib/util.js +++ b/lib/util.js @@ -101,7 +101,8 @@ export function dt(predicate, value, returnBoolean = false) { if (returnBoolean) { return predicate(value) } - return predicate(value) ? value : invalid + // normalize undefined and null + return predicate(value) ? (value === undefined ? null : value) : invalid } throw new Error(`${getName(predicate)} is a ${typeof predicate}, not a function. Expected predicate`) } diff --git a/test/regex/alt.test.js b/test/regex/alt.test.js index a0d1023..ad80420 100644 --- a/test/regex/alt.test.js +++ b/test/regex/alt.test.js @@ -89,6 +89,26 @@ describe("alt", () => { }) }) + it("works with null/undefined", () => { + // expect not nullable spec to be invalid for nil + expect(conform(ingredient_part, [])).to.deep.equal(invalid) + expect(conform(ingredient_part)).to.deep.equal(invalid) + + // expect nullable spec to be valid for nil + const nullable_alt = alt("value", p.number, "no value", p.nil) + expect(conform(nullable_alt, null)).to.deep.equal({ + "no value": null + }) + expect(conform(nullable_alt)).to.deep.equal({ + "no value": null + }) + expect(conform(nullable_alt, 5)).to.deep.equal({ + "value": 5 + }) + + + }) + it("works in happy nested case", () => { expect(conform(ingredient_variation, [5, "spoons"]), "regular").to.deep.equal({ regular: { diff --git a/test/regex/cat.test.js b/test/regex/cat.test.js index 1bea1df..24e06d9 100644 --- a/test/regex/cat.test.js +++ b/test/regex/cat.test.js @@ -1,6 +1,7 @@ import { expect } from 'chai' import { catImpl as cat, altImpl as alt } from '../../lib/regex' import map from '../../lib/spec/map' +import nilable from '../../lib/spec/nilable' import { conform, valid } from '../../lib/util' import { invalid } from '../../lib/symbols' import { define } from '../../lib/registry' @@ -80,6 +81,21 @@ describe("cat", () => { }) }) + it("works with nilable parts", () => { + // what if the spec, but not the regex accepts nil value? + const can_be_nil = nilable(map({ + quantity: p.number, + unit: p.string + })) + // expect(conform(can_be_nil)).to.deep.equal(undefined) + const nil_and_something = cat("something", p.string, "maybe nil", can_be_nil) + // console.log(explainData(nil_and_something, ["foo"])) + expect(conform(nil_and_something, ["foo"])).to.deep.equal({ + something: "foo", + "maybe nil": null + }) + }) + it("works in negative case (not matching preds)", () => { expect(conform(ingredient, ["5", "spoons"])).to.equal(invalid) }) @@ -132,7 +148,15 @@ describe("cat", () => { expect(problems).to.be.an("array").and.to.have.length(0) }) - it("[too few values]", () => { + it("[too few values, but it's ok]", () => { + const regular = alt("str", p.string, "int", p.number) + const nilable_named_ingredient = cat("name", p.string, "ingredient", alt("data", regular, "none", p.nil)) + const problems = explainData(nilable_named_ingredient, ["endless void"]) + + expect(problems).to.be.an("array").and.to.have.length(0) + }) + + it("[too few values, not ok]", () => { const problems = explainData(weak_ingredient, ["spoons"]) expect(problems).to.be.an("array").and.to.have.length(1) diff --git a/test/regex/maybe.test.js b/test/regex/maybe.test.js new file mode 100644 index 0000000..413db5a --- /dev/null +++ b/test/regex/maybe.test.js @@ -0,0 +1,41 @@ +import { maybeImpl as maybe, catImpl as cat } from '../../lib/regex' +import map from '../../lib/spec/map' +import { conform, valid } from '../../lib/util' +import { invalid } from '../../lib/symbols' +import { expect } from 'chai' +import { define } from '../../lib/registry' +import { explainData } from '../../index' +import * as p from '../../lib/predicates' + +const maybe_ingredient = maybe("ingredient", cat("quantity", p.number, "unit", p.string)) + +describe('maybe', () => { + describe('conform', () => { + it('works in happy case', () => { + expect(conform(maybe_ingredient), "no value").to.deep.equal({ + ingredient: null + }) + expect(conform(maybe_ingredient, []), "empty array").to.deep.equal({ + ingredient: null + }) + expect(conform(maybe_ingredient, [5, "spoons"]), "value").to.deep.equal({ + ingredient: { + unit: "spoons", + quantity: 5 + } + }) + }) + + it('works in nested case', () => { + const ingredient = cat("quantity", maybe("value", p.number), "unit", maybe("value", p.string)) + expect(conform(ingredient, [])).to.deep.equal({ + quantity: { + value: null + }, + unit: { + value: null + } + }) + }) + }) +}) diff --git a/test/spec/nilable.test.js b/test/spec/nilable.test.js index 05cf27f..db8c685 100644 --- a/test/spec/nilable.test.js +++ b/test/spec/nilable.test.js @@ -12,7 +12,7 @@ const friend = map({ const nil_friend = nilable(friend) const nil_int = nilable(p.int) -describe("tuple", () => { +describe("nilable", () => { describe("explain", () => { describe("works on specs", () => { it("[not null and not a friend]", () => { @@ -35,16 +35,16 @@ describe("tuple", () => { describe("conform", () => { it("works on predicates", () => { expect(nil_int.conform(null), "null").to.equal(null) - expect(nil_int.conform(undefined), "undefined").to.equal(undefined) - expect(nil_int.conform(), "undefined 2").to.equal(undefined) + expect(nil_int.conform(undefined), "undefined").to.equal(null) + expect(nil_int.conform(), "undefined 2").to.equal(null) expect(nil_int.conform(13), "int").to.equal(13) expect(nil_int.conform("13"), "string").to.equal(invalid) }) it("works on specs", () => { expect(nil_friend.conform(null), "null").to.equal(null) - expect(nil_friend.conform(undefined), "undefined").to.equal(undefined) - expect(nil_friend.conform(), "undefined 2").to.equal(undefined) + expect(nil_friend.conform(undefined), "undefined").to.equal(null) + expect(nil_friend.conform(), "undefined 2").to.equal(null) expect(nil_friend.conform({ name: "niko" }), "friend").to.deep.equal({ From a85864e809ea35ab36aa9254e03b53b15eddf022 Mon Sep 17 00:00:00 2001 From: David Normington Date: Wed, 12 Apr 2017 18:02:45 +0100 Subject: [PATCH 2/7] Initial work on rep * --- .gitignore | 2 ++ lib/regex.js | 50 +++++++++++++++++++++++++++-- test/regex/zeroOrMore.test.js | 60 +++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 test/regex/zeroOrMore.test.js diff --git a/.gitignore b/.gitignore index 922f11f..6dfd088 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ jspm_packages # Optional REPL history .node_repl_history + +*.swp diff --git a/lib/regex.js b/lib/regex.js index 8bcbc43..001a883 100644 --- a/lib/regex.js +++ b/lib/regex.js @@ -81,6 +81,21 @@ function altDeriv(regex, x) { }) } +function repDeriv(regex, x) { + let { ret } = regex + if (x == null) { + return regex; + } + + if (regex.predicate(x)) { + ret = ret.concat([x]); + } else { + ret = invalid; + } + + return Object.assign({}, regex, { ret, value: x }); +} + /** * Calculates derivative of regex with respect to x. @@ -105,6 +120,8 @@ export function deriv(regex, x) { return altDeriv(regex, x) case cat: return catDeriv(regex, x) + case rep: + return repDeriv(regex, x) } throw new Error(`Cannot derive unknown operation "${symbolToString(op)}"`) } @@ -118,6 +135,7 @@ function getReturn(regex) { return catProcessed case acc: case alt: + case rep: return regex.ret } throw new Error(`Return for ${symbolToString(regex.op)} not implemented`) @@ -187,6 +205,24 @@ export function regexExplain(regex, path, via, value) { return regex.ps.map((p, i) => explain(p, [...path, i], [...via, getName(regex), regex.ks[i]], value)) case acc: return null + + case rep: + return value + .reduce((errs, val, i) => { + let r = repDeriv(regex, val); + if (r.ret === invalid) { + r = Object.assign({}, r, { i }); + return errs.concat([r]); + } + + return errs; + }, []) + .map(regex => ({ + predicate: regex.predicate, + path: [...path, regex.i], + via, + value: regex.value, + })); } } @@ -199,6 +235,8 @@ function acceptsNil(regex) { return regex.ps.some(p => acceptsNil(p)) case cat: return regex.ps.every(p => acceptsNil(p)) + case rep: + return true; default: // TODO return false @@ -285,6 +323,10 @@ function palt(opts = {}) { return _alt } +function pZeroOrMore(opts) { + return opts; +} + // xy export function catImpl(...predicates) { if (p.odd(predicates.length)) { @@ -312,8 +354,12 @@ export function altImpl(...predicates) { } // x* -export function kleeneImpl() { - +export function zeroOrMoreImpl(predicate) { + return pZeroOrMore({ + op: rep, + predicate, + ret: [], + }); } // x+ diff --git a/test/regex/zeroOrMore.test.js b/test/regex/zeroOrMore.test.js new file mode 100644 index 0000000..5ff8987 --- /dev/null +++ b/test/regex/zeroOrMore.test.js @@ -0,0 +1,60 @@ +import { expect } from 'chai' +import { + zeroOrMoreImpl as zeroOrMore, + catImpl as cat, + altImpl as alt +} from '../../lib/regex' +import map from '../../lib/spec/map' +import nilable from '../../lib/spec/nilable' +import { conform, valid } from '../../lib/util' +import { invalid } from '../../lib/symbols' +import { define } from '../../lib/registry' +import { explainData, explain } from '../../index' +import * as p from '../../lib/predicates' + +describe.only('zeroOrMore', () => { + /* + star nil [] nil + star [] [] nil + star [:k] [:k] nil + star [:k1 :k2] [:k1 :k2] nil + star [:k1 :k2 "x"] ::s/invalid '[{:pred keyword?, :val "x" :via []}] + star ["a"] ::s/invalid '[{:pred keyword?, :val "a" :via []}] + */ + + it('no value', () => { + expect(conform(zeroOrMore(p.int))).to.eql([]); + expect(conform(zeroOrMore(p.int), null)).to.eql([]); + expect(conform(zeroOrMore(p.int), undefined)).to.eql([]); + }); + + it('empty array', () => { + expect(conform(zeroOrMore(p.int), [])).to.eql([]); + }); + + it('single correct value', () => { + expect(conform(zeroOrMore(p.int), [42])).to.eql([42]); + }); + + it('multiple correct values', () => { + expect(conform(zeroOrMore(p.int), [42, 21])).to.eql([42, 21]); + }); + + it('multiple incorrect values', () => { + expect(conform(zeroOrMore(p.int), [42, 21, 'x'])).to.eql(invalid); + const a = explainData(zeroOrMore(p.int), [42, 21, 'x']) + expect(a[0].predicate).to.eql(p.int); + expect(a[0].path).to.eql([2]); + expect(a[0].via).to.eql([]); + expect(a[0].value).to.eql('x'); + }); + + it('single incorrect value', () => { + expect(conform(zeroOrMore(p.int), ['x'])).to.eql(invalid); + const a = explainData(zeroOrMore(p.int), ['x']) + expect(a[0].predicate).to.eql(p.int); + expect(a[0].path).to.eql([0]); + expect(a[0].via).to.eql([]); + expect(a[0].value).to.eql('x'); + }); +}); From 503ffda5ea5a80f97357a4f5446a420c11dde1a3 Mon Sep 17 00:00:00 2001 From: David Normington Date: Thu, 13 Apr 2017 17:56:58 +0100 Subject: [PATCH 3/7] Bit closer --- lib/regex.js | 62 +++++++++++++----------- test/regex/kleene.test.js | 89 +++++++++++++++++++++++++++++++++++ test/regex/zeroOrMore.test.js | 60 ----------------------- 3 files changed, 123 insertions(+), 88 deletions(-) create mode 100644 test/regex/kleene.test.js delete mode 100644 test/regex/zeroOrMore.test.js diff --git a/lib/regex.js b/lib/regex.js index 001a883..5e1639e 100644 --- a/lib/regex.js +++ b/lib/regex.js @@ -1,12 +1,16 @@ // http://www.ccs.neu.edu/home/turon/re-deriv.pdf -import { dt, symbolToString, zip, valid, explain, getName } from './util' +import { + dt, symbolToString, zip, + valid, explain, getName, + specize, conform +} from './util' import { Nilable } from './spec/nilable' import { invalid } from './symbols' import * as p from './predicates' // ops const amp = Symbol('amp &') -const rep = Symbol('rep *') +const kleene = Symbol('kleene *') const alt = Symbol('alt |') const cat = Symbol('cat ·') const acc = Symbol('accepted') @@ -81,19 +85,13 @@ function altDeriv(regex, x) { }) } -function repDeriv(regex, x) { - let { ret } = regex - if (x == null) { - return regex; - } - - if (regex.predicate(x)) { - ret = ret.concat([x]); - } else { - ret = invalid; - } - - return Object.assign({}, regex, { ret, value: x }); +function kleeneDeriv(regex, x) { + const d = deriv(regex.predicate, x); + const c = pcat({ + ps: [d, regex], + ret: regex.ret, + }); + return c; } @@ -120,8 +118,8 @@ export function deriv(regex, x) { return altDeriv(regex, x) case cat: return catDeriv(regex, x) - case rep: - return repDeriv(regex, x) + case kleene: + return kleeneDeriv(regex, x) } throw new Error(`Cannot derive unknown operation "${symbolToString(op)}"`) } @@ -135,7 +133,7 @@ function getReturn(regex) { return catProcessed case acc: case alt: - case rep: + case kleene: return regex.ret } throw new Error(`Return for ${symbolToString(regex.op)} not implemented`) @@ -154,6 +152,7 @@ export function regexConform(regex, data) { if (xrest.length > 0) { return regexConform(dx, xrest) } + if (acceptsNil(dx)) { return getReturn(dx) } @@ -206,10 +205,10 @@ export function regexExplain(regex, path, via, value) { case acc: return null - case rep: + case kleene: return value .reduce((errs, val, i) => { - let r = repDeriv(regex, val); + let r = kleeneDeriv(regex, val); if (r.ret === invalid) { r = Object.assign({}, r, { i }); return errs.concat([r]); @@ -235,7 +234,7 @@ function acceptsNil(regex) { return regex.ps.some(p => acceptsNil(p)) case cat: return regex.ps.every(p => acceptsNil(p)) - case rep: + case kleene: return true; default: // TODO @@ -261,6 +260,7 @@ function pcat(opts = {}) { ret: pret }) } + // there are no more values // we convert the array of matches to a single map and return that return accept(getReturn(pcat({ @@ -323,10 +323,6 @@ function palt(opts = {}) { return _alt } -function pZeroOrMore(opts) { - return opts; -} - // xy export function catImpl(...predicates) { if (p.odd(predicates.length)) { @@ -353,10 +349,20 @@ export function altImpl(...predicates) { }) } +function pKleene(opts) { + let { predicate = () => true, ret = [] } = opts; + + return { + op: kleene, + predicate, + ret, + }; +} + // x* -export function zeroOrMoreImpl(predicate) { - return pZeroOrMore({ - op: rep, +export function kleeneImpl(predicate) { + return pKleene({ + op: kleene, predicate, ret: [], }); diff --git a/test/regex/kleene.test.js b/test/regex/kleene.test.js new file mode 100644 index 0000000..8361b26 --- /dev/null +++ b/test/regex/kleene.test.js @@ -0,0 +1,89 @@ +import { expect } from 'chai' +import { + kleeneImpl as kleene, + catImpl as cat, + altImpl as alt +} from '../../lib/regex' +import map from '../../lib/spec/map' +import nilable from '../../lib/spec/nilable' +import { conform, valid } from '../../lib/util' +import { invalid } from '../../lib/symbols' +import { define } from '../../lib/registry' +import { explainData, explain, spec } from '../../index' +import * as p from '../../lib/predicates' + +describe.only('kleene', () => { + /* + star nil [] nil + star [] [] nil + star [:k] [:k] nil + star [:k1 :k2] [:k1 :k2] nil + star [:k1 :k2 "x"] ::s/invalid '[{:pred keyword?, :val "x" :via []}] + star ["a"] ::s/invalid '[{:pred keyword?, :val "a" :via []}] + */ + + it('no value', () => { + expect(conform(kleene(p.int))).to.eql([]); + expect(conform(kleene(p.int), null)).to.eql([]); + expect(conform(kleene(p.int), undefined)).to.eql([]); + }); + + it('empty array', () => { + expect(conform(kleene(p.int), [])).to.eql([]); + }); + + it('single correct value', () => { + expect(conform(kleene(p.int), [42])).to.eql([42]); + }); + + it('with specs', () => { + expect(conform(kleene(spec.and(p.int, p.even)), [2])).to.eql([2]); + expect(conform(kleene(spec.and(p.int, p.even)), [3])).to.eql(invalid); + + expect(conform(kleene(spec.or({ + 'id': p.int, + 'name': p.string + })), [2])).to.eql([['id', 2]]); + expect(conform(kleene(spec.or({ + 'id': p.int, + 'name': p.string + })), [2, 'Barry'])).to.eql([['id', 2],['name', 'Barry']]); + }); + + it.only('with regexes', () => { + /* console.log(conform(alt('a', p.int, 'b', cat('c', p.string, 'd', p.int)), ['hi', 1])); */ + /* expect( */ + console.log(conform(kleene(cat( + 'word1', p.string, + 'id', p.int, + 'word2', p.string + )), ['hi', 3, 'world'])); + /* ).to.eql([{ */ + /* 'word1': 'hi', */ + /* 'id': 3, */ + /* 'word2': 'world' */ + /* }]); */ + }); + + it('multiple correct values', () => { + expect(conform(kleene(p.int), [42, 21])).to.eql([42, 21]); + }); + + it('multiple incorrect values', () => { + expect(conform(kleene(p.int), [42, 21, 'x'])).to.eql(invalid); + const a = explainData(kleene(p.int), [42, 21, 'x']) + expect(a[0].predicate).to.eql(p.int); + expect(a[0].path).to.eql([2]); + expect(a[0].via).to.eql([]); + expect(a[0].value).to.eql('x'); + }); + + it('single incorrect value', () => { + expect(conform(kleene(p.int), ['x'])).to.eql(invalid); + const a = explainData(kleene(p.int), ['x']) + expect(a[0].predicate).to.eql(p.int); + expect(a[0].path).to.eql([0]); + expect(a[0].via).to.eql([]); + expect(a[0].value).to.eql('x'); + }); +}); diff --git a/test/regex/zeroOrMore.test.js b/test/regex/zeroOrMore.test.js deleted file mode 100644 index 5ff8987..0000000 --- a/test/regex/zeroOrMore.test.js +++ /dev/null @@ -1,60 +0,0 @@ -import { expect } from 'chai' -import { - zeroOrMoreImpl as zeroOrMore, - catImpl as cat, - altImpl as alt -} from '../../lib/regex' -import map from '../../lib/spec/map' -import nilable from '../../lib/spec/nilable' -import { conform, valid } from '../../lib/util' -import { invalid } from '../../lib/symbols' -import { define } from '../../lib/registry' -import { explainData, explain } from '../../index' -import * as p from '../../lib/predicates' - -describe.only('zeroOrMore', () => { - /* - star nil [] nil - star [] [] nil - star [:k] [:k] nil - star [:k1 :k2] [:k1 :k2] nil - star [:k1 :k2 "x"] ::s/invalid '[{:pred keyword?, :val "x" :via []}] - star ["a"] ::s/invalid '[{:pred keyword?, :val "a" :via []}] - */ - - it('no value', () => { - expect(conform(zeroOrMore(p.int))).to.eql([]); - expect(conform(zeroOrMore(p.int), null)).to.eql([]); - expect(conform(zeroOrMore(p.int), undefined)).to.eql([]); - }); - - it('empty array', () => { - expect(conform(zeroOrMore(p.int), [])).to.eql([]); - }); - - it('single correct value', () => { - expect(conform(zeroOrMore(p.int), [42])).to.eql([42]); - }); - - it('multiple correct values', () => { - expect(conform(zeroOrMore(p.int), [42, 21])).to.eql([42, 21]); - }); - - it('multiple incorrect values', () => { - expect(conform(zeroOrMore(p.int), [42, 21, 'x'])).to.eql(invalid); - const a = explainData(zeroOrMore(p.int), [42, 21, 'x']) - expect(a[0].predicate).to.eql(p.int); - expect(a[0].path).to.eql([2]); - expect(a[0].via).to.eql([]); - expect(a[0].value).to.eql('x'); - }); - - it('single incorrect value', () => { - expect(conform(zeroOrMore(p.int), ['x'])).to.eql(invalid); - const a = explainData(zeroOrMore(p.int), ['x']) - expect(a[0].predicate).to.eql(p.int); - expect(a[0].path).to.eql([0]); - expect(a[0].via).to.eql([]); - expect(a[0].value).to.eql('x'); - }); -}); From 893a71128d2c512c889493de1c6b3c62cbffbb86 Mon Sep 17 00:00:00 2001 From: David Normington Date: Tue, 18 Apr 2017 13:20:20 +0100 Subject: [PATCH 4/7] All kleene tests passing --- lib/regex.js | 88 +++++++++++++++++++++++++-------------- lib/util.js | 6 ++- test/regex/kleene.test.js | 37 ++++++---------- 3 files changed, 74 insertions(+), 57 deletions(-) diff --git a/lib/regex.js b/lib/regex.js index 5e1639e..8b173e4 100644 --- a/lib/regex.js +++ b/lib/regex.js @@ -86,12 +86,19 @@ function altDeriv(regex, x) { } function kleeneDeriv(regex, x) { - const d = deriv(regex.predicate, x); - const c = pcat({ - ps: [d, regex], - ret: regex.ret, - }); - return c; + const { p1, p2, ret } = regex; + + let ps = [ + pkleene({ p1: deriv(p1, x), p2, ret }) + ]; + + if (acceptsNil(p1)) { + ps = ps.concat([ + deriv(pkleene({ p1: p2, p2, ret: ret.concat([getReturn(p1)]) }), x) + ]); + } + + return ps.length > 1 ? palt({ ps }) : ps[0]; } @@ -133,8 +140,10 @@ function getReturn(regex) { return catProcessed case acc: case alt: + return regex.ret; case kleene: - return regex.ret + const p1ret = accepted(regex.p1) ? getReturn(regex.p1) : null; + return regex.ret.concat(p1ret ? [p1ret] : []); } throw new Error(`Return for ${symbolToString(regex.op)} not implemented`) } @@ -144,19 +153,35 @@ function getReturn(regex) { * matches data, invalid otherwise. */ export function regexConform(regex, data) { - const [x0, ...xrest] = data - const dx = deriv(regex, x0) - if (accepted(dx)) { - return dx.ret - } - if (xrest.length > 0) { - return regexConform(dx, xrest) + if (data !== null && !Array.isArray(data)) { + return invalid; } - if (acceptsNil(dx)) { - return getReturn(dx) + const [x0, ...xrest] = data + + if (!data.length) { + if (acceptsNil(regex)) { + return getReturn(regex); + } else { + return invalid; + } + } else { + const dx = deriv(regex, x0); + return dx ? regexConform(dx, xrest) : invalid; } - return invalid + + // const dx = deriv(regex, x0) + // if (accepted(dx)) { + // return dx.ret + // } + // if (xrest.length > 0) { + // return regexConform(dx, xrest) + // } + + // if (acceptsNil(dx)) { + // return getReturn(dx) + // } + // return invalid } export function regexExplain(regex, path, via, value) { @@ -209,15 +234,16 @@ export function regexExplain(regex, path, via, value) { return value .reduce((errs, val, i) => { let r = kleeneDeriv(regex, val); - if (r.ret === invalid) { - r = Object.assign({}, r, { i }); + + if (r.p1 === null) { + r = Object.assign({}, r, { i, value: val }); return errs.concat([r]); } return errs; }, []) .map(regex => ({ - predicate: regex.predicate, + predicate: regex.p2, path: [...path, regex.i], via, value: regex.value, @@ -235,7 +261,7 @@ function acceptsNil(regex) { case cat: return regex.ps.every(p => acceptsNil(p)) case kleene: - return true; + return regex.p1 === regex.p2 || acceptsNil(regex.p1); default: // TODO return false @@ -349,22 +375,22 @@ export function altImpl(...predicates) { }) } -function pKleene(opts) { - let { predicate = () => true, ret = [] } = opts; +function pkleene(opts) { + let { p1, p2, ret = [] } = opts; - return { - op: kleene, - predicate, - ret, - }; + const r = { op: kleene, p1, p2, ret }; + + return accepted(p1) + ? Object.assign({}, r, { p1: p2, ret: ret.concat([getReturn(p1)]) }) + : r; } // x* export function kleeneImpl(predicate) { - return pKleene({ + return pkleene({ op: kleene, - predicate, - ret: [], + p1: predicate, + p2: predicate, }); } diff --git a/lib/util.js b/lib/util.js index ea4996d..ea29ba3 100644 --- a/lib/util.js +++ b/lib/util.js @@ -72,12 +72,14 @@ export function getName(thing) { } function toArray(value) { - return p.array(value) ? value : [value] + return value === undefined + ? [] + : p.array(value) ? value : [value] } export function conform(spec, value) { if (isRegex(spec)) { - return regexConform(spec, toArray(value)) + return regexConform(spec, toArray(value == null ? [] : value)) } return specize(spec).conform(value) } diff --git a/test/regex/kleene.test.js b/test/regex/kleene.test.js index 8361b26..1f51b42 100644 --- a/test/regex/kleene.test.js +++ b/test/regex/kleene.test.js @@ -12,20 +12,10 @@ import { define } from '../../lib/registry' import { explainData, explain, spec } from '../../index' import * as p from '../../lib/predicates' -describe.only('kleene', () => { - /* - star nil [] nil - star [] [] nil - star [:k] [:k] nil - star [:k1 :k2] [:k1 :k2] nil - star [:k1 :k2 "x"] ::s/invalid '[{:pred keyword?, :val "x" :via []}] - star ["a"] ::s/invalid '[{:pred keyword?, :val "a" :via []}] - */ - +describe('kleene', () => { it('no value', () => { expect(conform(kleene(p.int))).to.eql([]); expect(conform(kleene(p.int), null)).to.eql([]); - expect(conform(kleene(p.int), undefined)).to.eql([]); }); it('empty array', () => { @@ -50,19 +40,18 @@ describe.only('kleene', () => { })), [2, 'Barry'])).to.eql([['id', 2],['name', 'Barry']]); }); - it.only('with regexes', () => { - /* console.log(conform(alt('a', p.int, 'b', cat('c', p.string, 'd', p.int)), ['hi', 1])); */ - /* expect( */ - console.log(conform(kleene(cat( - 'word1', p.string, - 'id', p.int, - 'word2', p.string - )), ['hi', 3, 'world'])); - /* ).to.eql([{ */ - /* 'word1': 'hi', */ - /* 'id': 3, */ - /* 'word2': 'world' */ - /* }]); */ + it('with regexes', () => { + expect( + conform(kleene(cat( + 'word1', p.string, + 'id', p.int, + 'word2', p.string + )), ['hi', 3, 'world']) + ).to.eql([{ + 'word1': 'hi', + 'id': 3, + 'word2': 'world' + }]); }); it('multiple correct values', () => { From 212f9aab910f83cb8d56a7066abef3c67cdf79e0 Mon Sep 17 00:00:00 2001 From: David Normington Date: Tue, 18 Apr 2017 16:18:57 +0100 Subject: [PATCH 5/7] Checked faling tests, fixed some, added TODO comments for others --- lib/regex.js | 15 ++------------- test/regex/alt.test.js | 7 +++++-- test/regex/cat.test.js | 1 + test/regex/maybe.test.js | 2 ++ 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/lib/regex.js b/lib/regex.js index 8b173e4..dfe8023 100644 --- a/lib/regex.js +++ b/lib/regex.js @@ -169,19 +169,6 @@ export function regexConform(regex, data) { const dx = deriv(regex, x0); return dx ? regexConform(dx, xrest) : invalid; } - - // const dx = deriv(regex, x0) - // if (accepted(dx)) { - // return dx.ret - // } - // if (xrest.length > 0) { - // return regexConform(dx, xrest) - // } - - // if (acceptsNil(dx)) { - // return getReturn(dx) - // } - // return invalid } export function regexExplain(regex, path, via, value) { @@ -256,6 +243,8 @@ function acceptsNil(regex) { return regex === p.nil || regex instanceof Nilable } switch (regex.op) { + case acc: + return true; case alt: return regex.ps.some(p => acceptsNil(p)) case cat: diff --git a/test/regex/alt.test.js b/test/regex/alt.test.js index ad80420..8fd196e 100644 --- a/test/regex/alt.test.js +++ b/test/regex/alt.test.js @@ -84,6 +84,8 @@ describe("alt", () => { }) it("works with too many items provided", () => { + // TODO this diverges from clojure.spec + // @see https://github.com/clojure/clojure/blob/master/test/clojure/test_clojure/spec.clj#L95 expect(conform(ingredient_part, [5, "spoons", "tops"])).to.deep.equal({ quantity: 5 }) @@ -99,14 +101,14 @@ describe("alt", () => { expect(conform(nullable_alt, null)).to.deep.equal({ "no value": null }) + + // TODO I don't think we want null and undefined to have the same meaning expect(conform(nullable_alt)).to.deep.equal({ "no value": null }) expect(conform(nullable_alt, 5)).to.deep.equal({ "value": 5 }) - - }) it("works in happy nested case", () => { @@ -140,6 +142,7 @@ describe("alt", () => { }) it("[too many values]", () => { + // TODO again this diverges from clojure.spec const problems = explainData(ingredient_variation, ["spoons", 5, 5]) expect(problems).to.be.an("array").and.to.have.length(0) }) diff --git a/test/regex/cat.test.js b/test/regex/cat.test.js index 24e06d9..6221a5f 100644 --- a/test/regex/cat.test.js +++ b/test/regex/cat.test.js @@ -105,6 +105,7 @@ describe("cat", () => { }) it("works with too many items provided", () => { + // TODO this also diverges from clojure.spec expect(conform(ingredient, [5, "spoons", "tops"])).to.deep.equal({ quantity: 5, unit: "spoons" diff --git a/test/regex/maybe.test.js b/test/regex/maybe.test.js index 413db5a..14414fa 100644 --- a/test/regex/maybe.test.js +++ b/test/regex/maybe.test.js @@ -12,6 +12,7 @@ const maybe_ingredient = maybe("ingredient", cat("quantity", p.number, "unit", p describe('maybe', () => { describe('conform', () => { it('works in happy case', () => { + // TODO diverges from clojure.spec expect(conform(maybe_ingredient), "no value").to.deep.equal({ ingredient: null }) @@ -28,6 +29,7 @@ describe('maybe', () => { it('works in nested case', () => { const ingredient = cat("quantity", maybe("value", p.number), "unit", maybe("value", p.string)) + // TODO again mixing undefined and null values here expect(conform(ingredient, [])).to.deep.equal({ quantity: { value: null From 4a235abad28b90d2f5a3d34eece9978f67755276 Mon Sep 17 00:00:00 2001 From: David Normington Date: Tue, 18 Apr 2017 16:55:47 +0100 Subject: [PATCH 6/7] kleene nilable --- lib/regex.js | 4 +--- test/regex/cat.test.js | 4 ++++ test/regex/kleene.test.js | 4 ++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/regex.js b/lib/regex.js index dfe8023..de19b67 100644 --- a/lib/regex.js +++ b/lib/regex.js @@ -239,9 +239,7 @@ export function regexExplain(regex, path, via, value) { } function acceptsNil(regex) { - if (!isRegex(regex)) { - return regex === p.nil || regex instanceof Nilable - } + regex = regex || {}; switch (regex.op) { case acc: return true; diff --git a/test/regex/cat.test.js b/test/regex/cat.test.js index 6221a5f..14cfb53 100644 --- a/test/regex/cat.test.js +++ b/test/regex/cat.test.js @@ -90,6 +90,10 @@ describe("cat", () => { // expect(conform(can_be_nil)).to.deep.equal(undefined) const nil_and_something = cat("something", p.string, "maybe nil", can_be_nil) // console.log(explainData(nil_and_something, ["foo"])) + + // TODO this should be an error according to clojure.spec + // ['foo' null] => correct! + // ['foo'] => Insufficient input expect(conform(nil_and_something, ["foo"])).to.deep.equal({ something: "foo", "maybe nil": null diff --git a/test/regex/kleene.test.js b/test/regex/kleene.test.js index 1f51b42..a86f030 100644 --- a/test/regex/kleene.test.js +++ b/test/regex/kleene.test.js @@ -54,6 +54,10 @@ describe('kleene', () => { }]); }); + it('with nilable', () => { + expect(conform(kleene(nilable(p.int)), [null])).to.eql([null]); + }); + it('multiple correct values', () => { expect(conform(kleene(p.int), [42, 21])).to.eql([42, 21]); }); From bf73c13fdc337f6a1c6c07adfd3359b75b5898e6 Mon Sep 17 00:00:00 2001 From: David Normington Date: Tue, 18 Apr 2017 17:00:40 +0100 Subject: [PATCH 7/7] Double checking maybe vs nilable --- lib/regex.js | 1 + test/regex/kleene.test.js | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/regex.js b/lib/regex.js index de19b67..abfb2d8 100644 --- a/lib/regex.js +++ b/lib/regex.js @@ -387,6 +387,7 @@ export function plusImpl() { } // x? +// TODO should there be a name parameter for maybe? export function maybeImpl(name, predicate) { if (p.nil(name) || !p.string(name)) { throw new Error(`Must provide a name to maybe.`) diff --git a/test/regex/kleene.test.js b/test/regex/kleene.test.js index a86f030..135f524 100644 --- a/test/regex/kleene.test.js +++ b/test/regex/kleene.test.js @@ -2,7 +2,8 @@ import { expect } from 'chai' import { kleeneImpl as kleene, catImpl as cat, - altImpl as alt + altImpl as alt, + maybeImpl as maybe } from '../../lib/regex' import map from '../../lib/spec/map' import nilable from '../../lib/spec/nilable' @@ -16,6 +17,7 @@ describe('kleene', () => { it('no value', () => { expect(conform(kleene(p.int))).to.eql([]); expect(conform(kleene(p.int), null)).to.eql([]); + expect(conform(kleene(p.int), [null])).to.eql(invalid); }); it('empty array', () => { @@ -56,6 +58,7 @@ describe('kleene', () => { it('with nilable', () => { expect(conform(kleene(nilable(p.int)), [null])).to.eql([null]); + expect(conform(kleene(maybe('i', p.int)), [])).to.eql([]); }); it('multiple correct values', () => {