diff options
| -rw-r--r-- | src/parser/index.ts | 38 | ||||
| -rw-r--r-- | src/parser/utils.ts | 29 | ||||
| -rw-r--r-- | src/types.ts | 9 | ||||
| -rw-r--r-- | tests/parser.spec.ts | 79 |
4 files changed, 108 insertions, 47 deletions
diff --git a/src/parser/index.ts b/src/parser/index.ts index c61364d..3943848 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -2,9 +2,11 @@ import { flow, pipe } from 'fp-ts/function' import { chain, left, orElse, right } from 'fp-ts/lib/Either' import { delimited, + digits, many0, many1, mapTo, + matchChar, oneOf, optional, or, @@ -18,7 +20,9 @@ import { tuple3, whitespaces0, } from './utils' -import { Expr, ListExpr } from '../types' +import { Expr, ListExpr, Literal } from '../types' +import {getOrElse, map} from 'fp-ts/lib/Option' +import {snd} from 'fp-ts/lib/Tuple' const start = mapTo(symbol('^'), _ => Expr.Start()) const end = mapTo(symbol('$'), _ => Expr.End()) @@ -76,6 +80,37 @@ const objectProperty = (input: string) => ), ) +const unsignedNum: Parser<number> = mapTo(pair(digits, optional(pair(matchChar('.'), digits))), ([int, decimal]) => + pipe( + decimal, + map(snd), + getOrElse(() => '0'), + n => parseFloat(`${int}.${n}`), + ) +) + +const numberLiteral: Parser<Literal> = mapTo( + pair(optional(oneOf(['+', '-'])), unsignedNum), + ([signO, n]) => + pipe( + signO, + getOrElse(() => '+'), + sign => (sign === '-' ? -1 : 1) * n, + Literal.Number, + ) + , +) + +const booleanLiteral: Parser<Literal> = mapTo(oneOf(['true', 'false']), b => + Literal.Boolean(b === 'true' ? true : false), +) + +const literalP: Parser<Literal> = delimited( + whitespaces0, + or([booleanLiteral, numberLiteral]), + whitespaces0, +) + const expressionP: Parser<Expr> = (input: string) => pipe( input, @@ -91,6 +126,7 @@ const expressionP: Parser<Expr> = (input: string) => anyBool, truthy, falsey, + mapTo(literalP, Expr.Literal), ]), wrapQuantifiers, wrapAlt, diff --git a/src/parser/utils.ts b/src/parser/utils.ts index 841d77c..908405e 100644 --- a/src/parser/utils.ts +++ b/src/parser/utils.ts @@ -10,7 +10,7 @@ import { orElseW, } from 'fp-ts/Either' import { none, some, Option } from 'fp-ts/Option' -import { mapFst, mapSnd, snd } from 'fp-ts/Tuple' +import { fst, mapFst, mapSnd, snd } from 'fp-ts/Tuple' import { eq } from '../utils' export type char = string @@ -46,7 +46,7 @@ export const many1 = <T>(parser: Parser<T>): Parser<Array<T>> => chain(([res, inp]) => res.length > 0 ? right([res, inp]) - : left([`many1 failed to parse at ${inp}`, inp]), + : left([`many1 failed to parse at '${inp}'`, inp]), ), ), ) @@ -66,21 +66,16 @@ const recoverInput = ) export const prefixed = <T>(a: Parser<any>, b: Parser<T>): Parser<T> => - recoverInput(flow(a, chain(flow(snd, b)))) + recoverInput(mapTo(pair(a, b), snd)) export const suffixed = <T>(a: Parser<T>, b: Parser<any>): Parser<T> => - recoverInput( - flow( - a, - chain(([out, inp]) => pipe(inp, b, map(mapFst(_ => out)))), - ), - ) + recoverInput(mapTo(pair(a, b), fst)) export const delimited = <T>( p: Parser<any>, a: Parser<T>, s: Parser<any>, -): Parser<T> => recoverInput(suffixed(prefixed(p, a), s)) +): Parser<T> => suffixed(prefixed(p, a), s) export const satifyChar = (f: (c: char) => boolean): Parser<char> => @@ -92,9 +87,9 @@ export const satifyChar = export const digit = satifyChar(c => /^[0-9]$/g.test(c)) -export const integer: Parser<number> = flow( +export const digits: Parser<string> = flow( many1(digit), - map(mapFst(ds => parseInt(ds.join(''), 10))), + map(mapFst(ds => ds.join(''))), ) export const or = <T>(parsers: Parser<T>[]): Parser<T> => { @@ -111,13 +106,6 @@ export const or = <T>(parsers: Parser<T>[]): Parser<T> => { export const matchChar = (ch: char): Parser<char> => satifyChar(eq(ch)) -export const space = matchChar(' ') -export const newline = matchChar('\n') -export const tab = matchChar('\t') - -export const whitespace = or([space, newline, tab]) -export const whitespaces0 = many0(whitespace) - export const matchString = (s: string): Parser<string> => (input: string) => @@ -127,6 +115,9 @@ export const matchString = export const oneOf = (xs: string[]): Parser<string> => or(xs.map(matchString)) +export const whitespace = oneOf([' ', '\n', '\t']) +export const whitespaces0 = many0(whitespace) + export const symbol = (s: string): Parser<string> => delimited(whitespaces0, matchString(s), whitespaces0) diff --git a/src/types.ts b/src/types.ts index d30413d..ac577a1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,6 +3,13 @@ import { constructors, Union } from "./utils" type _ = never +export type Literal = Union<{ + String: string, + Number: number, + Boolean: boolean, +}> +export const Literal = constructors<Literal>() + export type Expr = Union<{ Start: _, End: _, @@ -19,8 +26,8 @@ export type Expr = Union<{ Falsey: _, Group: { exprs: Expr[] }, PropertyMatch: { name: string, exprs: Expr[] }, + Literal: Literal, }> - export const Expr = constructors<Expr>() export type ListExpr = [ diff --git a/tests/parser.spec.ts b/tests/parser.spec.ts index 9bc891e..c935165 100644 --- a/tests/parser.spec.ts +++ b/tests/parser.spec.ts @@ -1,31 +1,30 @@ import { left, right } from 'fp-ts/Either' import { - integer, + digits, whitespace, whitespaces0, delimited, - many1, - satifyChar, } from '../src/parser/utils' -import { parser, propertyName } from '../src/parser' +import { parser } from '../src/parser' import { none, some } from 'fp-ts/Option' -import { Expr } from '../src/types' +import { Expr, Literal } from '../src/types' +import { jlog } from '../src/utils' -const plog = (x: any) => console.log(JSON.stringify(x, null, 2)) +const wrap = (l: any) => right([[none, l, none], '']) -describe('Foobar', () => { +describe('Parser', () => { it('should do shit', () => { - expect(integer('12901')).toEqual(right([12901, ''])) - expect(integer('12901asas')).toEqual(right([12901, 'asas'])) + expect(digits('12901')).toEqual(right(['12901', ''])) + expect(digits('12901asas')).toEqual(right(['12901', 'asas'])) expect(whitespace(' ')).toEqual(right([' ', ''])) expect(whitespace('\n')).toEqual(right(['\n', ''])) expect(whitespace('a')).toEqual(left(['unable to match', 'a'])) - expect(delimited(whitespaces0, integer, whitespaces0)(' 20 ')).toEqual( - right([20, '']), + expect(delimited(whitespaces0, digits, whitespaces0)(' 20 ')).toEqual( + right(['20', '']), ) - expect(delimited(whitespaces0, integer, whitespaces0)(' 2 0 ')).toEqual( - right([2, '0 ']), + expect(delimited(whitespaces0, digits, whitespaces0)(' 2 0 ')).toEqual( + right(['2', '0 ']), ) }) @@ -87,20 +86,48 @@ describe('Foobar', () => { it('object proprtyu', () => { expect(parser(/ [name \s\T] [age \n] /.source)).toEqual( - right([ - [ - none, - [ - Expr.PropertyMatch({ - name: 'name', - exprs: [Expr.AnyString(), Expr.Truthy()], - }), - Expr.PropertyMatch({ name: 'age', exprs: [Expr.AnyNumber()] }), - ], - none, - ], - '', + wrap([ + Expr.PropertyMatch({ + name: 'name', + exprs: [Expr.AnyString(), Expr.Truthy()], + }), + Expr.PropertyMatch({ name: 'age', exprs: [Expr.AnyNumber()] }), ]), ) }) + + it('literals', () => { + // unsigned numbers + expect(parser(/ 0.105 /.source)).toEqual( + wrap([Expr.Literal(Literal.Number(0.105))]), + ) + expect(parser(/ 9 /.source)).toEqual( + wrap([Expr.Literal(Literal.Number(9))]), + ) + expect(parser(/ 23 /.source)).toEqual( + wrap([Expr.Literal(Literal.Number(23))]), + ) + + // signed numbers + expect(parser(/ +23.025 /.source)).toEqual( + wrap([Expr.Literal(Literal.Number(23.025))]), + ) + expect(parser(/ -23.025 /.source)).toEqual( + wrap([Expr.Literal(Literal.Number(-23.025))]), + ) + expect(parser(/ +23 /.source)).toEqual( + wrap([Expr.Literal(Literal.Number(23))]), + ) + expect(parser(/ -23 /.source)).toEqual( + wrap([Expr.Literal(Literal.Number(-23))]), + ) + + // boolean + expect(parser(/ true /.source)).toEqual( + wrap([Expr.Literal(Literal.Boolean(true))]), + ) + expect(parser(/ false /.source)).toEqual( + wrap([Expr.Literal(Literal.Boolean(false))]), + ) + }) }) |
