Merge parser and expressions into repository

This commit is contained in:
Christopher Schleiden
2023-01-06 15:54:31 -08:00
parent 4f961a6247
commit 70e33f999d
328 changed files with 35337 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
import {
ExpressionData,
ExpressionDataInterface,
Kind,
kindStr,
} from "./expressiondata";
export class Array implements ExpressionDataInterface {
private v: ExpressionData[] = [];
constructor(...data: ExpressionData[]) {
for (const d of data) {
this.add(d);
}
}
public readonly kind = Kind.Array;
public primitive = false;
coerceString(): string {
return kindStr(this.kind);
}
number(): number {
return NaN;
}
add(value: ExpressionData) {
this.v.push(value);
}
get(index: number): ExpressionData {
return this.v[index];
}
values(): ExpressionData[] {
return this.v;
}
}
+25
View File
@@ -0,0 +1,25 @@
import { ExpressionDataInterface, Kind } from "./expressiondata";
export class BooleanData implements ExpressionDataInterface {
constructor(public readonly value: boolean) {}
public readonly kind = Kind.Boolean;
public primitive = true;
coerceString(): string {
if (this.value) {
return "true";
}
return "false";
}
number(): number {
if (this.value) {
return 1;
}
return 0;
}
}
@@ -0,0 +1,11 @@
import { Dictionary } from "./dictionary";
import { StringData } from "./string";
describe("dictionary", () => {
it("pairs contains all values", () => {
const d = new Dictionary();
d.add("ABC", new StringData("val"));
expect(d.pairs()).toEqual([{ key: "ABC", value: new StringData("val") }]);
});
});
@@ -0,0 +1,68 @@
import {
ExpressionData,
ExpressionDataInterface,
Kind,
kindStr,
Pair,
} from "./expressiondata";
export class Dictionary implements ExpressionDataInterface {
private keys: string[] = [];
private v: ExpressionData[] = [];
private indexMap: { [key: string]: number } = {};
constructor(...pairs: Pair[]) {
for (const p of pairs) {
this.add(p.key, p.value);
}
}
public readonly kind = Kind.Dictionary;
public primitive = false;
coerceString(): string {
return kindStr(this.kind);
}
number(): number {
return NaN;
}
add(key: string, value: ExpressionData) {
if (this.indexMap[key.toLowerCase()]) {
return;
}
this.keys.push(key);
this.v.push(value);
this.indexMap[key.toLowerCase()] = this.v.length - 1;
}
get(key: string): ExpressionData | undefined {
const index = this.indexMap[key.toLowerCase()];
if (index === undefined) {
return undefined;
}
return this.v[index];
}
values(): ExpressionData[] {
return this.v;
}
pairs(): Pair[] {
const result: Pair[] = [];
for (const key of this.keys) {
result.push({ key, value: this.v[this.indexMap[key.toLowerCase()]] });
}
return result;
}
}
export function isDictionary(x: ExpressionData): x is Dictionary {
return x.kind === Kind.Dictionary;
}
@@ -0,0 +1,57 @@
import { Dictionary } from "./dictionary";
import { Null } from "./null";
import { Array } from "./array";
import { StringData } from "./string";
import { NumberData } from "./number";
import { BooleanData } from "./boolean";
export enum Kind {
String = 0,
Array,
Dictionary,
Boolean,
Number,
CaseSensitiveDictionary,
Null,
}
export function kindStr(k: Kind): string {
switch (k) {
case Kind.Array:
return "Array";
case Kind.Boolean:
return "Boolean";
case Kind.Null:
return "Null";
case Kind.Number:
return "Number";
case Kind.Dictionary:
return "Object";
case Kind.String:
return "String";
}
return "unknown";
}
export interface ExpressionDataInterface {
kind: Kind;
primitive: boolean;
coerceString(): string;
number(): number;
}
export type ExpressionData =
| Array
| Dictionary
| StringData
| BooleanData
| NumberData
| Null;
export type Pair = {
key: string;
value: ExpressionData;
};
+9
View File
@@ -0,0 +1,9 @@
export { Array } from "./array";
export { BooleanData } from "./boolean";
export { Dictionary } from "./dictionary";
export { ExpressionData, Kind } from "./expressiondata";
export { Null } from "./null";
export { NumberData } from "./number";
export { replacer } from "./replacer";
export { reviver } from "./reviver";
export { StringData } from "./string";
+21
View File
@@ -0,0 +1,21 @@
import {
ExpressionData,
ExpressionDataInterface,
Kind,
} from "./expressiondata";
export class Null implements ExpressionDataInterface {
constructor() {}
public readonly kind = Kind.Null;
public primitive = true;
coerceString(): string {
return "";
}
number(): number {
return 0;
}
}
+23
View File
@@ -0,0 +1,23 @@
import { ExpressionDataInterface, Kind } from "./expressiondata";
export class NumberData implements ExpressionDataInterface {
constructor(public readonly value: number) {}
public readonly kind = Kind.Number;
public primitive = true;
coerceString(): string {
if (this.value === -0) {
return "0";
}
// Workaround to limit the precision to at most 15 digits. Format the number to a string, then parse
// it back to a number to remove trailing zeroes to prevent numbers to be converted to 1.200000000...
return (+this.value.toFixed(15)).toString();
}
number(): number {
return this.value;
}
}
@@ -0,0 +1,32 @@
import { Array } from "./array";
import { Dictionary } from "./dictionary";
import { Null } from "./null";
import { NumberData } from "./number";
import { replacer } from "./replacer";
import { StringData } from "./string";
describe("replacer", () => {
it("null", () => {
expect(JSON.stringify(new Null(), replacer, " ")).toEqual("null");
});
it("array", () => {
expect(
JSON.stringify(
new Array(new StringData("a"), new StringData("b")),
replacer,
" "
)
).toEqual('[\n "a",\n "b"\n]');
});
it("dictionary", () => {
expect(
JSON.stringify(
new Dictionary({ key: "a", value: new NumberData(42) }),
replacer,
" "
)
).toEqual('{\n "a": 42\n}');
});
});
+46
View File
@@ -0,0 +1,46 @@
import { Array } from "./array";
import { BooleanData } from "./boolean";
import { Dictionary } from "./dictionary";
import { Null } from "./null";
import { NumberData } from "./number";
import { StringData } from "./string";
/**
* Replacer can be passed to JSON.stringify to convert an ExpressionData object into plain JSON
*
* See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#replacer
*/
export function replacer(key: string, value: any): any {
if (value instanceof Null) {
return null;
}
if (value instanceof BooleanData) {
return value.value;
}
if (value instanceof NumberData) {
return value.number();
}
if (value instanceof StringData) {
return value.coerceString();
}
if (value instanceof Array) {
return value.values();
}
if (value instanceof Dictionary) {
const pairs = value.pairs();
const r: any = {};
for (const p of pairs) {
r[p.key] = p.value;
}
return r;
}
return value;
}
@@ -0,0 +1,84 @@
import { Array } from "./array";
import { BooleanData } from "./boolean";
import { Dictionary } from "./dictionary";
import { ExpressionData } from "./expressiondata";
import { Null } from "./null";
import { NumberData } from "./number";
import { reviver } from "./reviver";
import { StringData } from "./string";
describe("reviver", () => {
const tests: {
name: string;
data: string;
want: ExpressionData;
}[] = [
{
name: "null",
data: "null",
want: new Null(),
},
{
name: "number",
data: "1",
want: new NumberData(1),
},
{
name: "string",
data: `"a"`,
want: new StringData("a"),
},
{
name: "true boolean",
data: "true",
want: new BooleanData(true),
},
{
name: "false boolean",
data: "false",
want: new BooleanData(false),
},
{
name: "array",
data: `[1,2,3]`,
want: new Array(new NumberData(1), new NumberData(2), new NumberData(3)),
},
{
name: "nested array",
data: `[1,2,[3,4],5]`,
want: new Array(
new NumberData(1),
new NumberData(2),
new Array(new NumberData(3), new NumberData(4)),
new NumberData(5)
),
},
{
name: "complex array",
data: `[{"a":[true,2]},{"b":"three"},{"c":null}]`,
want: new Array(
new Dictionary({
key: "a",
value: new Array(new BooleanData(true), new NumberData(2)),
}),
new Dictionary({ key: "b", value: new StringData("three") }),
new Dictionary({ key: "c", value: new Null() })
),
},
{
name: "dictionary",
data: `{ "object1": {} }`,
want: new Dictionary({
key: "object1",
value: new Dictionary(),
}),
},
];
test.each(tests)(
"$name",
({ data, want }: { data: string; want: ExpressionData }) => {
expect(JSON.parse(data, reviver)).toEqual(want);
}
);
});
+49
View File
@@ -0,0 +1,49 @@
import { Array as dArray } from "./array";
import { BooleanData } from "./boolean";
import { Dictionary } from "./dictionary";
import { ExpressionData, Pair } from "./expressiondata";
import { Null } from "./null";
import { NumberData } from "./number";
import { StringData } from "./string";
/**
* Reviver can be passed to `JSON.parse` to convert plain JSON into an `ExpressionData` object.
*
* See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#reviver
*/
export function reviver(key: string, val: any): ExpressionData {
if (val === null) {
return new Null();
}
if (typeof val === "string") {
return new StringData(val);
}
if (typeof val === "number") {
return new NumberData(val);
}
if (typeof val === "boolean") {
return new BooleanData(val as boolean);
}
if (Array.isArray(val)) {
return new dArray(...val);
}
if (typeof val === "object") {
return new Dictionary(
...Object.keys(val).map(
(k) =>
({
key: k,
value: val[k],
} as Pair)
)
);
}
// Pass through value
return val;
}
+17
View File
@@ -0,0 +1,17 @@
import { ExpressionDataInterface, Kind } from "./expressiondata";
export class StringData implements ExpressionDataInterface {
constructor(public readonly value: string) {}
public readonly kind = Kind.String;
public primitive = true;
coerceString(): string {
return this.value;
}
number(): number {
return Number(this.value);
}
}