Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,5 @@ jspm_packages

# Optional REPL history
.node_repl_history

*.swp
138 changes: 120 additions & 18 deletions lib/regex.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
})
}

Expand All @@ -63,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.
Expand All @@ -87,10 +120,25 @@ 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)}"`)
}

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:
case rep:
return regex.ret
}
throw new Error(`Return for ${symbolToString(regex.op)} not implemented`)
}

/**
Expand All @@ -106,12 +154,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
}
Expand Down Expand Up @@ -155,6 +205,41 @@ 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,
}));
}
}

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))
case rep:
return true;
default:
// TODO
return false
}
}

Expand All @@ -178,9 +263,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,
Expand Down Expand Up @@ -219,7 +305,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) {
Expand All @@ -238,6 +323,10 @@ function palt(opts = {}) {
return _alt
}

function pZeroOrMore(opts) {
return opts;
}

// xy
export function catImpl(...predicates) {
if (p.odd(predicates.length)) {
Expand Down Expand Up @@ -265,8 +354,12 @@ export function altImpl(...predicates) {
}

// x*
export function kleeneImpl() {

export function zeroOrMoreImpl(predicate) {
return pZeroOrMore({
op: rep,
predicate,
ret: [],
});
}

// x+
Expand All @@ -275,8 +368,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]
})
}

//????
Expand Down
2 changes: 1 addition & 1 deletion lib/spec/nilable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
3 changes: 2 additions & 1 deletion lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
}
Expand Down
20 changes: 20 additions & 0 deletions test/regex/alt.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
26 changes: 25 additions & 1 deletion test/regex/cat.test.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)
})
Expand Down Expand Up @@ -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)
Expand Down
41 changes: 41 additions & 0 deletions test/regex/maybe.test.js
Original file line number Diff line number Diff line change
@@ -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
}
})
})
})
})
Loading