parjs
Version:
A parser-combinator library for JavaScript.
169 lines (148 loc) • 6.9 kB
text/typescript
/**
* @module parjs
*/
/** */
import {AsciiCodes} from "char-info/ascii";
import {NumericHelpers} from "./numeric-helpers";
import {ResultKind} from "../result";
import {ParsingState} from "../state";
import defaults from "lodash/defaults";
import {ParjserBase} from "../parser";
import {Parjser} from "../parjser";
/**
* A set of options for parsing floating point numbers.
*/
export interface FloatOptions {
allowSign: boolean;
allowImplicitZero: boolean;
allowFloatingPoint: boolean;
allowExponent: boolean;
}
const defaultFloatOptions: FloatOptions = {
allowExponent: true,
allowSign: true,
allowImplicitZero: true,
allowFloatingPoint: true
};
const msgOneOrMoreDigits = "one or more digits";
const msgExponentSign = "exponent sign (+ or -)";
/*
This work is really better done using Parjs itself, but it's wrapped in (mostly) a single parser for efficiency purposes.
We want a configurable number parser that can parse floating point numbers in any base, with or without a sign, and with or without
an exponent...
Here are the rules of this parser.
Replace {1,2 3, 4} by the digits allowed with the base, which is configurable.
BASIC NUMBER FORMS - parser must be one of:
a. 1234 : integer
b. 12.3 : floating point, allowed if {allowFloatingPoint}.
c. .123 : floating point, implicit whole part. Requires {allowFloatingPoint && allowImplicitZero}
d. 123. : floating point, implicit fractional part. Requires {allowFloatingPoint && allowImplicitZero}
CONFIGURABLE EXTRAS:
a. Sign prefix: (+|-) preceeding the number. Allowed if {allowSign}.
b. Exponent suffix: (e|E)(+|-)\d+. Allowed if {allowExponent}. Can be combined with {!allowFloatingPoint}.
FAILURES:
a. '' - no characters consumed. Parser fails.
b. '.' - could be understood as an implicit 0.0, but will not be parsed by this parser.
c. '1e+' -
with {allowExponent}, this fails after consuming '1e+' because it expected an integer after the + but didn't find one.
without that flag, this succeeds and parses just 1.
SUCCESSES:
a. '1abc' - The parser will just consume and return 1.
b. '10e+1' - {allowExponent} can be true even if {allowFloatingPoint} isn't.
ISSUES:
a. If base >= 15 then the character 'e' is a digit and so {allowExponent} must be false since it cannot be parsed.
Otherwise, an error is thrown.
b.
*/
/**
* Returns a parser that will parse a single floating point number, in decimal
* or scientific form.
* @param options Options for parsing the floating-point number.
*/
export function float(options: Partial<FloatOptions> = defaultFloatOptions): Parjser<number> {
options = defaults(options, defaultFloatOptions);
return new class Float extends ParjserBase {
type = "float";
expecting = "expecting a floating-point number";
_apply(ps: ParsingState): void {
let {allowSign, allowFloatingPoint, allowImplicitZero, allowExponent} = options;
let {position, input} = ps;
if (position >= input.length) {
ps.kind = ResultKind.SoftFail;
return;
}
let initPos = position;
let sign = 1;
let hasSign = false, hasWhole = false, hasFraction = false;
if (allowSign) {
// try parse a sign
sign = NumericHelpers.parseSign(ps);
if (sign === 0) {
sign = 1;
} else {
hasSign = true;
}
}
// after a sign there needs to come an integer part (if any).
let prevPos = ps.position;
NumericHelpers.parseDigitsInBase(ps, 10);
hasWhole = ps.position !== prevPos;
// now if allowFloatingPoint, we try to parse a decimal point.
let nextChar = input.charCodeAt(ps.position);
prevPos = ps.position;
if (!allowImplicitZero && !hasWhole) {
// fail because we don't allow ".1", and similar without allowImplicitZero.
ps.kind = hasSign ? ResultKind.HardFail : ResultKind.SoftFail;
ps.reason = msgOneOrMoreDigits;
return;
}
// tslint:disable-next-line:label-position
floatingParse: {
if (allowFloatingPoint && nextChar === AsciiCodes.decimalPoint) {
// skip to the char after the decimal point
ps.position++;
let prevFractionalPos = ps.position;
// parse the fractional part
NumericHelpers.parseDigitsInBase(ps, 10);
hasFraction = prevFractionalPos !== ps.position;
if (!allowImplicitZero && !hasFraction) {
// we encountered something like 212. but allowImplicitZero is false.
// that means we need to backtrack to the . character and succeed in parsing the integer.
// the remainder is not a valid number.
break floatingParse;
}
// after parseDigits has been invoked, the ps.position is on the next character (which could be e).
nextChar = input.charCodeAt(ps.position);
prevPos = ps.position;
}
if (!hasWhole && !hasFraction) {
// even if allowImplicitZero is true, we still don't parse '.' as '0.0'.
ps.kind = hasSign ? ResultKind.HardFail : ResultKind.SoftFail;
ps.reason = msgOneOrMoreDigits;
return;
}
// note that if we don't allow floating point, the char that might've been '.' will instead be 'e' or 'E'.
// if we do allow floating point, then the previous block would've consumed some characters.
if (allowExponent && (nextChar === AsciiCodes.e || nextChar === AsciiCodes.E)) {
ps.position++;
let expSign = NumericHelpers.parseSign(ps);
if (expSign === 0) {
ps.kind = ResultKind.HardFail;
ps.reason = msgExponentSign;
return;
}
let prevFractionalPos = ps.position;
NumericHelpers.parseDigitsInBase(ps, 10);
if (ps.position === prevFractionalPos) {
// we parsed e+ but we did not parse any digits.
ps.kind = ResultKind.HardFail;
ps.reason = msgOneOrMoreDigits;
return;
}
}
}
ps.kind = ResultKind.Ok;
ps.value = parseFloat(input.substring(initPos, ps.position));
}
}();
}