chronolizer 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -119
- package/dist/ast/constructors.d.mts +135 -0
- package/dist/ast/constructors.mjs +25 -0
- package/dist/ast/fold.d.mts +13 -0
- package/dist/ast/fold.mjs +58 -0
- package/dist/ast/normalize.d.mts +42 -0
- package/dist/ast/normalize.mjs +34 -0
- package/dist/ast/schemas.d.mts +80 -0
- package/dist/ast/schemas.mjs +65 -0
- package/dist/filter/codec.d.mts +92 -0
- package/dist/filter/codec.mjs +48 -0
- package/dist/filter/errors.d.mts +14 -0
- package/dist/filter/errors.mjs +10 -0
- package/dist/filter/expression.d.mts +8 -0
- package/dist/filter/expression.mjs +76 -0
- package/dist/filter/schema.d.mts +13 -0
- package/dist/filter/schema.mjs +11 -0
- package/dist/filter/transformation.d.mts +37 -0
- package/dist/filter/transformation.mjs +20 -0
- package/dist/index.d.mts +22 -969
- package/dist/index.mjs +22 -2305
- package/dist/language/errors.d.mts +38 -0
- package/dist/language/errors.mjs +30 -0
- package/dist/language/model.d.mts +239 -0
- package/dist/language/model.mjs +50 -0
- package/dist/language/registry.d.mts +17 -0
- package/dist/language/registry.mjs +116 -0
- package/dist/locales/cs.d.mts +10 -0
- package/dist/locales/cs.mjs +746 -0
- package/dist/locales/de.d.mts +10 -0
- package/dist/locales/de.mjs +1039 -0
- package/dist/locales/en.d.mts +10 -0
- package/dist/locales/en.mjs +598 -0
- package/dist/locales/es.d.mts +10 -0
- package/dist/locales/es.mjs +908 -0
- package/dist/locales/fr.d.mts +10 -0
- package/dist/locales/fr.mjs +901 -0
- package/dist/locales/nl.d.mts +10 -0
- package/dist/locales/nl.mjs +791 -0
- package/dist/locales/pl.d.mts +10 -0
- package/dist/locales/pl.mjs +885 -0
- package/dist/locales/shared.mjs +395 -0
- package/dist/locales/tr.d.mts +10 -0
- package/dist/locales/tr.mjs +725 -0
- package/dist/natural/correction.d.mts +13 -0
- package/dist/natural/correction.mjs +73 -0
- package/dist/natural/format.d.mts +47 -0
- package/dist/natural/format.mjs +14 -0
- package/dist/natural/parse.d.mts +99 -0
- package/dist/natural/parse.mjs +66 -0
- package/dist/natural/policy.mjs +6 -0
- package/dist/natural/suggest.d.mts +53 -0
- package/dist/natural/suggest.mjs +24 -0
- package/dist/natural/suggestion.d.mts +4 -0
- package/dist/natural/suggestion.mjs +79 -0
- package/dist/natural/text.d.mts +5 -0
- package/dist/natural/text.mjs +5 -0
- package/dist/resolve/resolve.d.mts +79 -0
- package/dist/resolve/resolve.mjs +58 -0
- package/dist/resolve/schema.d.mts +59 -0
- package/dist/resolve/schema.mjs +28 -0
- package/package.json +15 -3
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Shift } from "./schemas.mjs";
|
|
2
|
+
import { boundedRange, dateLiteral, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, lowerOpenRange, now, shift, startOf, upperOpenRange } from "./constructors.mjs";
|
|
3
|
+
import { Match, Schema } from "effect";
|
|
4
|
+
//#region src/ast/normalize.ts
|
|
5
|
+
const isShift = Schema.is(Shift);
|
|
6
|
+
const normalizeInstant = (expression) => Match.valueTags(expression, {
|
|
7
|
+
Now: () => now(),
|
|
8
|
+
DateLiteral: (literal) => dateLiteral(literal.value),
|
|
9
|
+
StartOf: (operation) => startOf(normalizeInstant(operation.base), operation.unit),
|
|
10
|
+
Shift: (operation) => {
|
|
11
|
+
const base = normalizeInstant(operation.base);
|
|
12
|
+
if (operation.amount === 0) return base;
|
|
13
|
+
if (isShift(base) && base.unit === operation.unit) {
|
|
14
|
+
const amount = base.amount + operation.amount;
|
|
15
|
+
if (Number.isSafeInteger(amount)) return normalizeInstant(shift(base.base, amount, operation.unit));
|
|
16
|
+
}
|
|
17
|
+
return shift(base, operation.amount, operation.unit);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
const normalizeLower = (bound) => Match.valueTags(bound, {
|
|
21
|
+
GreaterThan: (value) => greaterThan(normalizeInstant(value.value)),
|
|
22
|
+
GreaterThanOrEqual: (value) => greaterThanOrEqual(normalizeInstant(value.value))
|
|
23
|
+
});
|
|
24
|
+
const normalizeUpper = (bound) => Match.valueTags(bound, {
|
|
25
|
+
LessThan: (value) => lessThan(normalizeInstant(value.value)),
|
|
26
|
+
LessThanOrEqual: (value) => lessThanOrEqual(normalizeInstant(value.value))
|
|
27
|
+
});
|
|
28
|
+
const normalizeRange = (range) => {
|
|
29
|
+
if (range.lower !== void 0 && range.upper !== void 0) return boundedRange(normalizeLower(range.lower), normalizeUpper(range.upper));
|
|
30
|
+
if (range.lower !== void 0) return lowerOpenRange(normalizeLower(range.lower));
|
|
31
|
+
return upperOpenRange(normalizeUpper(range.upper));
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { normalizeInstant, normalizeRange };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
//#region src/ast/schemas.d.ts
|
|
3
|
+
declare const Unit: Schema.Literals<readonly ["day", "week", "month", "quarter", "year"]>;
|
|
4
|
+
type Unit = typeof Unit.Type;
|
|
5
|
+
declare const daysInMonth: (year: number, month: number) => number;
|
|
6
|
+
declare const isIsoDate: (value: string) => boolean;
|
|
7
|
+
declare const IsoDate: Schema.String;
|
|
8
|
+
type IsoDate = typeof IsoDate.Type;
|
|
9
|
+
declare const Now_base: Schema.Class<Now, Schema.TaggedStruct<"Now", {}>, {}>;
|
|
10
|
+
declare class Now extends Now_base {}
|
|
11
|
+
declare const DateLiteral_base: Schema.Class<DateLiteral, Schema.TaggedStruct<"DateLiteral", {
|
|
12
|
+
readonly value: Schema.String;
|
|
13
|
+
}>, {}>;
|
|
14
|
+
declare class DateLiteral extends DateLiteral_base {}
|
|
15
|
+
declare const Shift_base: Schema.Class<Shift, Schema.TaggedStruct<"Shift", {
|
|
16
|
+
readonly base: Schema.suspend<Schema.Codec<InstantExpr, InstantExpr, never, never>>;
|
|
17
|
+
readonly amount: Schema.Int;
|
|
18
|
+
readonly unit: Schema.Literals<readonly ["day", "week", "month", "quarter", "year"]>;
|
|
19
|
+
}>, {}>;
|
|
20
|
+
declare class Shift extends Shift_base {}
|
|
21
|
+
declare const StartOf_base: Schema.Class<StartOf, Schema.TaggedStruct<"StartOf", {
|
|
22
|
+
readonly base: Schema.suspend<Schema.Codec<InstantExpr, InstantExpr, never, never>>;
|
|
23
|
+
readonly unit: Schema.Literals<readonly ["day", "week", "month", "quarter", "year"]>;
|
|
24
|
+
}>, {}>;
|
|
25
|
+
declare class StartOf extends StartOf_base {}
|
|
26
|
+
type InstantExpr = Now | DateLiteral | Shift | StartOf;
|
|
27
|
+
declare const InstantExpr: Schema.Codec<InstantExpr>;
|
|
28
|
+
declare const GreaterThan: Schema.TaggedStruct<"GreaterThan", {
|
|
29
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
30
|
+
}>;
|
|
31
|
+
declare const GreaterThanOrEqual: Schema.TaggedStruct<"GreaterThanOrEqual", {
|
|
32
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
33
|
+
}>;
|
|
34
|
+
declare const LessThan: Schema.TaggedStruct<"LessThan", {
|
|
35
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
36
|
+
}>;
|
|
37
|
+
declare const LessThanOrEqual: Schema.TaggedStruct<"LessThanOrEqual", {
|
|
38
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
39
|
+
}>;
|
|
40
|
+
declare const LowerBound: Schema.Union<readonly [Schema.TaggedStruct<"GreaterThan", {
|
|
41
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
42
|
+
}>, Schema.TaggedStruct<"GreaterThanOrEqual", {
|
|
43
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
44
|
+
}>]>;
|
|
45
|
+
type LowerBound = typeof LowerBound.Type;
|
|
46
|
+
declare const UpperBound: Schema.Union<readonly [Schema.TaggedStruct<"LessThan", {
|
|
47
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
48
|
+
}>, Schema.TaggedStruct<"LessThanOrEqual", {
|
|
49
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
50
|
+
}>]>;
|
|
51
|
+
type UpperBound = typeof UpperBound.Type;
|
|
52
|
+
declare const DateRangeExpr: Schema.Union<readonly [Schema.TaggedStruct<"DateRange", {
|
|
53
|
+
readonly lower: Schema.Union<readonly [Schema.TaggedStruct<"GreaterThan", {
|
|
54
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
55
|
+
}>, Schema.TaggedStruct<"GreaterThanOrEqual", {
|
|
56
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
57
|
+
}>]>;
|
|
58
|
+
readonly upper: Schema.Union<readonly [Schema.TaggedStruct<"LessThan", {
|
|
59
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
60
|
+
}>, Schema.TaggedStruct<"LessThanOrEqual", {
|
|
61
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
62
|
+
}>]>;
|
|
63
|
+
}>, Schema.TaggedStruct<"DateRange", {
|
|
64
|
+
readonly lower: Schema.Union<readonly [Schema.TaggedStruct<"GreaterThan", {
|
|
65
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
66
|
+
}>, Schema.TaggedStruct<"GreaterThanOrEqual", {
|
|
67
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
68
|
+
}>]>;
|
|
69
|
+
readonly upper: Schema.optionalKey<Schema.Never>;
|
|
70
|
+
}>, Schema.TaggedStruct<"DateRange", {
|
|
71
|
+
readonly lower: Schema.optionalKey<Schema.Never>;
|
|
72
|
+
readonly upper: Schema.Union<readonly [Schema.TaggedStruct<"LessThan", {
|
|
73
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
74
|
+
}>, Schema.TaggedStruct<"LessThanOrEqual", {
|
|
75
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
76
|
+
}>]>;
|
|
77
|
+
}>]>;
|
|
78
|
+
type DateRangeExpr = typeof DateRangeExpr.Type;
|
|
79
|
+
//#endregion
|
|
80
|
+
export { DateLiteral, DateRangeExpr, GreaterThan, GreaterThanOrEqual, InstantExpr, IsoDate, LessThan, LessThanOrEqual, LowerBound, Now, Shift, StartOf, Unit, UpperBound, daysInMonth, isIsoDate };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Match, Option, Schema, String } from "effect";
|
|
2
|
+
//#region src/ast/schemas.ts
|
|
3
|
+
const Unit = Schema.Literals([
|
|
4
|
+
"day",
|
|
5
|
+
"week",
|
|
6
|
+
"month",
|
|
7
|
+
"quarter",
|
|
8
|
+
"year"
|
|
9
|
+
]);
|
|
10
|
+
const isLeapYear = (year) => year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
11
|
+
const daysInMonth = (year, month) => Match.value(month).pipe(Match.when(2, () => isLeapYear(year) ? 29 : 28), Match.when(Match.is(4, 6, 9, 11), () => 30), Match.orElse(() => 31));
|
|
12
|
+
const isIsoDate = (value) => {
|
|
13
|
+
if (Option.isNone(String.match(/^\d{4}-\d{2}-\d{2}$/)(value))) return false;
|
|
14
|
+
const year = Number(value.slice(0, 4));
|
|
15
|
+
const month = Number(value.slice(5, 7));
|
|
16
|
+
const day = Number(value.slice(8, 10));
|
|
17
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth(year, month);
|
|
18
|
+
};
|
|
19
|
+
const IsoDate = Schema.String.check(Schema.makeFilter(isIsoDate, { expected: "an ISO calendar date (YYYY-MM-DD)" })).annotate({ identifier: "IsoDate" });
|
|
20
|
+
var Now = class extends Schema.TaggedClass()("Now", {}) {};
|
|
21
|
+
var DateLiteral = class extends Schema.TaggedClass()("DateLiteral", { value: IsoDate }) {};
|
|
22
|
+
const ShiftAmount = Schema.Int.check(Schema.isBetween({
|
|
23
|
+
minimum: Number.MIN_SAFE_INTEGER,
|
|
24
|
+
maximum: Number.MAX_SAFE_INTEGER
|
|
25
|
+
}));
|
|
26
|
+
var Shift = class extends Schema.TaggedClass()("Shift", {
|
|
27
|
+
base: Schema.suspend(() => InstantExpr),
|
|
28
|
+
amount: ShiftAmount,
|
|
29
|
+
unit: Unit
|
|
30
|
+
}) {};
|
|
31
|
+
var StartOf = class extends Schema.TaggedClass()("StartOf", {
|
|
32
|
+
base: Schema.suspend(() => InstantExpr),
|
|
33
|
+
unit: Unit
|
|
34
|
+
}) {};
|
|
35
|
+
const InstantExpr = Schema.suspend(() => Schema.Union([
|
|
36
|
+
Now,
|
|
37
|
+
DateLiteral,
|
|
38
|
+
Shift,
|
|
39
|
+
StartOf
|
|
40
|
+
])).annotate({ identifier: "InstantExpr" });
|
|
41
|
+
const GreaterThan = Schema.TaggedStruct("GreaterThan", { value: InstantExpr });
|
|
42
|
+
const GreaterThanOrEqual = Schema.TaggedStruct("GreaterThanOrEqual", { value: InstantExpr });
|
|
43
|
+
const LessThan = Schema.TaggedStruct("LessThan", { value: InstantExpr });
|
|
44
|
+
const LessThanOrEqual = Schema.TaggedStruct("LessThanOrEqual", { value: InstantExpr });
|
|
45
|
+
const LowerBound = Schema.Union([GreaterThan, GreaterThanOrEqual]);
|
|
46
|
+
const UpperBound = Schema.Union([LessThan, LessThanOrEqual]);
|
|
47
|
+
const BoundedDateRange = Schema.TaggedStruct("DateRange", {
|
|
48
|
+
lower: LowerBound,
|
|
49
|
+
upper: UpperBound
|
|
50
|
+
});
|
|
51
|
+
const LowerOpenDateRange = Schema.TaggedStruct("DateRange", {
|
|
52
|
+
lower: LowerBound,
|
|
53
|
+
upper: Schema.optionalKey(Schema.Never)
|
|
54
|
+
});
|
|
55
|
+
const UpperOpenDateRange = Schema.TaggedStruct("DateRange", {
|
|
56
|
+
lower: Schema.optionalKey(Schema.Never),
|
|
57
|
+
upper: UpperBound
|
|
58
|
+
});
|
|
59
|
+
const DateRangeExpr = Schema.Union([
|
|
60
|
+
BoundedDateRange,
|
|
61
|
+
LowerOpenDateRange,
|
|
62
|
+
UpperOpenDateRange
|
|
63
|
+
]).annotate({ identifier: "DateRangeExpr" });
|
|
64
|
+
//#endregion
|
|
65
|
+
export { DateLiteral, DateRangeExpr, GreaterThan, GreaterThanOrEqual, InstantExpr, IsoDate, LessThan, LessThanOrEqual, LowerBound, Now, Shift, StartOf, Unit, UpperBound, daysInMonth, isIsoDate };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { DateRangeExpr, InstantExpr } from "../ast/schemas.mjs";
|
|
2
|
+
import { FilterExpressionParseError, InvalidDateFilterError } from "./errors.mjs";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
//#region src/filter/codec.d.ts
|
|
5
|
+
declare const parseFilter: (filter: {
|
|
6
|
+
readonly gt?: string;
|
|
7
|
+
readonly gte?: string;
|
|
8
|
+
readonly lt?: string;
|
|
9
|
+
readonly lte?: string;
|
|
10
|
+
}) => Effect.Effect<{
|
|
11
|
+
readonly _tag: "DateRange";
|
|
12
|
+
readonly lower: {
|
|
13
|
+
readonly _tag: "GreaterThan";
|
|
14
|
+
readonly value: InstantExpr;
|
|
15
|
+
} | {
|
|
16
|
+
readonly _tag: "GreaterThanOrEqual";
|
|
17
|
+
readonly value: InstantExpr;
|
|
18
|
+
};
|
|
19
|
+
readonly upper: {
|
|
20
|
+
readonly _tag: "LessThan";
|
|
21
|
+
readonly value: InstantExpr;
|
|
22
|
+
} | {
|
|
23
|
+
readonly _tag: "LessThanOrEqual";
|
|
24
|
+
readonly value: InstantExpr;
|
|
25
|
+
};
|
|
26
|
+
} | {
|
|
27
|
+
readonly _tag: "DateRange";
|
|
28
|
+
readonly lower: {
|
|
29
|
+
readonly _tag: "GreaterThan";
|
|
30
|
+
readonly value: InstantExpr;
|
|
31
|
+
} | {
|
|
32
|
+
readonly _tag: "GreaterThanOrEqual";
|
|
33
|
+
readonly value: InstantExpr;
|
|
34
|
+
};
|
|
35
|
+
readonly upper?: never;
|
|
36
|
+
} | {
|
|
37
|
+
readonly _tag: "DateRange";
|
|
38
|
+
readonly lower?: never;
|
|
39
|
+
readonly upper: {
|
|
40
|
+
readonly _tag: "LessThan";
|
|
41
|
+
readonly value: InstantExpr;
|
|
42
|
+
} | {
|
|
43
|
+
readonly _tag: "LessThanOrEqual";
|
|
44
|
+
readonly value: InstantExpr;
|
|
45
|
+
};
|
|
46
|
+
}, FilterExpressionParseError | InvalidDateFilterError, never>;
|
|
47
|
+
declare const formatFilter: (range: DateRangeExpr) => {
|
|
48
|
+
readonly gt?: string;
|
|
49
|
+
readonly gte?: string;
|
|
50
|
+
readonly lt?: string;
|
|
51
|
+
readonly lte?: string;
|
|
52
|
+
};
|
|
53
|
+
declare const rangeKey: (range: DateRangeExpr) => string;
|
|
54
|
+
declare const completePeriod: (start: InstantExpr, end: InstantExpr) => {
|
|
55
|
+
readonly _tag: "DateRange";
|
|
56
|
+
readonly lower: {
|
|
57
|
+
readonly _tag: "GreaterThan";
|
|
58
|
+
readonly value: InstantExpr;
|
|
59
|
+
} | {
|
|
60
|
+
readonly _tag: "GreaterThanOrEqual";
|
|
61
|
+
readonly value: InstantExpr;
|
|
62
|
+
};
|
|
63
|
+
readonly upper: {
|
|
64
|
+
readonly _tag: "LessThan";
|
|
65
|
+
readonly value: InstantExpr;
|
|
66
|
+
} | {
|
|
67
|
+
readonly _tag: "LessThanOrEqual";
|
|
68
|
+
readonly value: InstantExpr;
|
|
69
|
+
};
|
|
70
|
+
} | {
|
|
71
|
+
readonly _tag: "DateRange";
|
|
72
|
+
readonly lower: {
|
|
73
|
+
readonly _tag: "GreaterThan";
|
|
74
|
+
readonly value: InstantExpr;
|
|
75
|
+
} | {
|
|
76
|
+
readonly _tag: "GreaterThanOrEqual";
|
|
77
|
+
readonly value: InstantExpr;
|
|
78
|
+
};
|
|
79
|
+
readonly upper?: never;
|
|
80
|
+
} | {
|
|
81
|
+
readonly _tag: "DateRange";
|
|
82
|
+
readonly lower?: never;
|
|
83
|
+
readonly upper: {
|
|
84
|
+
readonly _tag: "LessThan";
|
|
85
|
+
readonly value: InstantExpr;
|
|
86
|
+
} | {
|
|
87
|
+
readonly _tag: "LessThanOrEqual";
|
|
88
|
+
readonly value: InstantExpr;
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
//#endregion
|
|
92
|
+
export { completePeriod, formatFilter, parseFilter, rangeKey };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { boundedRange, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, lowerOpenRange, upperOpenRange } from "../ast/constructors.mjs";
|
|
2
|
+
import { normalizeRange } from "../ast/normalize.mjs";
|
|
3
|
+
import { InvalidDateFilterError } from "./errors.mjs";
|
|
4
|
+
import { formatInstantExpression, parseInstantExpression } from "./expression.mjs";
|
|
5
|
+
import { DateFilter } from "./schema.mjs";
|
|
6
|
+
import { Effect, Match } from "effect";
|
|
7
|
+
//#region src/filter/codec.ts
|
|
8
|
+
const parseLower = (filter) => {
|
|
9
|
+
if (filter.gt !== void 0) return Effect.map(parseInstantExpression(filter.gt), greaterThan);
|
|
10
|
+
if (filter.gte !== void 0) return Effect.map(parseInstantExpression(filter.gte), greaterThanOrEqual);
|
|
11
|
+
return Effect.void;
|
|
12
|
+
};
|
|
13
|
+
const parseUpper = (filter) => {
|
|
14
|
+
if (filter.lt !== void 0) return Effect.map(parseInstantExpression(filter.lt), lessThan);
|
|
15
|
+
if (filter.lte !== void 0) return Effect.map(parseInstantExpression(filter.lte), lessThanOrEqual);
|
|
16
|
+
return Effect.void;
|
|
17
|
+
};
|
|
18
|
+
const parseFilter = Effect.fn("chronolizer.parseFilter")(function* (filter) {
|
|
19
|
+
const [lower, upper] = yield* Effect.all([parseLower(filter), parseUpper(filter)]);
|
|
20
|
+
if (lower !== void 0 && upper !== void 0) return normalizeRange(boundedRange(lower, upper));
|
|
21
|
+
if (lower !== void 0) return normalizeRange(lowerOpenRange(lower));
|
|
22
|
+
if (upper !== void 0) return normalizeRange(upperOpenRange(upper));
|
|
23
|
+
return yield* new InvalidDateFilterError({ message: "A date filter must contain at least one bound" });
|
|
24
|
+
});
|
|
25
|
+
const formatLower = Match.typeTags()({
|
|
26
|
+
GreaterThan: (bound) => DateFilter.make({ gt: formatInstantExpression(bound.value) }),
|
|
27
|
+
GreaterThanOrEqual: (bound) => DateFilter.make({ gte: formatInstantExpression(bound.value) })
|
|
28
|
+
});
|
|
29
|
+
const formatUpper = Match.typeTags()({
|
|
30
|
+
LessThan: (bound) => DateFilter.make({ lt: formatInstantExpression(bound.value) }),
|
|
31
|
+
LessThanOrEqual: (bound) => DateFilter.make({ lte: formatInstantExpression(bound.value) })
|
|
32
|
+
});
|
|
33
|
+
const formatFilter = (range) => {
|
|
34
|
+
const normalized = normalizeRange(range);
|
|
35
|
+
if (normalized.lower !== void 0 && normalized.upper !== void 0) return DateFilter.make({
|
|
36
|
+
...formatLower(normalized.lower),
|
|
37
|
+
...formatUpper(normalized.upper)
|
|
38
|
+
});
|
|
39
|
+
if (normalized.lower !== void 0) return formatLower(normalized.lower);
|
|
40
|
+
return formatUpper(normalized.upper);
|
|
41
|
+
};
|
|
42
|
+
const rangeKey = (range) => {
|
|
43
|
+
const filter = formatFilter(range);
|
|
44
|
+
return `${filter.gt ?? ""}|${filter.gte ?? ""}|${filter.lt ?? ""}|${filter.lte ?? ""}`;
|
|
45
|
+
};
|
|
46
|
+
const completePeriod = (start, end) => boundedRange(greaterThanOrEqual(start), lessThan(end));
|
|
47
|
+
//#endregion
|
|
48
|
+
export { completePeriod, formatFilter, parseFilter, rangeKey };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
//#region src/filter/errors.d.ts
|
|
3
|
+
declare const FilterExpressionParseError_base: Schema.Class<FilterExpressionParseError, Schema.TaggedStruct<"FilterExpressionParseError", {
|
|
4
|
+
readonly input: Schema.String;
|
|
5
|
+
readonly offset: Schema.Int;
|
|
6
|
+
readonly expected: Schema.String;
|
|
7
|
+
}>, import("effect/Cause").YieldableError>;
|
|
8
|
+
declare class FilterExpressionParseError extends FilterExpressionParseError_base {}
|
|
9
|
+
declare const InvalidDateFilterError_base: Schema.Class<InvalidDateFilterError, Schema.TaggedStruct<"InvalidDateFilterError", {
|
|
10
|
+
readonly message: Schema.String;
|
|
11
|
+
}>, import("effect/Cause").YieldableError>;
|
|
12
|
+
declare class InvalidDateFilterError extends InvalidDateFilterError_base {}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { FilterExpressionParseError, InvalidDateFilterError };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
//#region src/filter/errors.ts
|
|
3
|
+
var FilterExpressionParseError = class extends Schema.TaggedError()("FilterExpressionParseError", {
|
|
4
|
+
input: Schema.String,
|
|
5
|
+
offset: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
6
|
+
expected: Schema.String
|
|
7
|
+
}) {};
|
|
8
|
+
var InvalidDateFilterError = class extends Schema.TaggedError()("InvalidDateFilterError", { message: Schema.String }) {};
|
|
9
|
+
//#endregion
|
|
10
|
+
export { FilterExpressionParseError, InvalidDateFilterError };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { InstantExpr } from "../ast/schemas.mjs";
|
|
2
|
+
import { FilterExpressionParseError } from "./errors.mjs";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
//#region src/filter/expression.d.ts
|
|
5
|
+
declare const parseInstantExpression: (input: string) => Effect.Effect<InstantExpr, FilterExpressionParseError, never>;
|
|
6
|
+
declare const formatInstantExpression: (expression: InstantExpr) => string;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { formatInstantExpression, parseInstantExpression };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { isIsoDate } from "../ast/schemas.mjs";
|
|
2
|
+
import { dateLiteral, now, shift, startOf } from "../ast/constructors.mjs";
|
|
3
|
+
import { foldInstant } from "../ast/fold.mjs";
|
|
4
|
+
import { normalizeInstant } from "../ast/normalize.mjs";
|
|
5
|
+
import { FilterExpressionParseError } from "./errors.mjs";
|
|
6
|
+
import { Effect, Match } from "effect";
|
|
7
|
+
//#region src/filter/expression.ts
|
|
8
|
+
const unitFromSymbol = (symbol) => Match.value(symbol).pipe(Match.when("d", () => "day"), Match.when("w", () => "week"), Match.when("M", () => "month"), Match.when("q", () => "quarter"), Match.when("y", () => "year"), Match.orElse(() => void 0));
|
|
9
|
+
const symbolFromUnit = (unit) => Match.value(unit).pipe(Match.when("day", () => "d"), Match.when("week", () => "w"), Match.when("month", () => "M"), Match.when("quarter", () => "q"), Match.when("year", () => "y"), Match.exhaustive);
|
|
10
|
+
const failAt = (input, offset, expected) => Effect.fail(new FilterExpressionParseError({
|
|
11
|
+
input,
|
|
12
|
+
offset,
|
|
13
|
+
expected
|
|
14
|
+
}));
|
|
15
|
+
const isDigit = (value) => value >= "0" && value <= "9";
|
|
16
|
+
const isFirstPositiveDigit = (value) => value >= "1" && value <= "9";
|
|
17
|
+
const parseInstantExpression = Effect.fn(function* (input) {
|
|
18
|
+
let cursor = 0;
|
|
19
|
+
let expression;
|
|
20
|
+
let fixedAnchor = false;
|
|
21
|
+
if (input.startsWith("now")) {
|
|
22
|
+
expression = now();
|
|
23
|
+
cursor = 3;
|
|
24
|
+
} else {
|
|
25
|
+
const candidate = input.slice(0, 10);
|
|
26
|
+
if (!isIsoDate(candidate)) return yield* failAt(input, 0, "\"now\" or an ISO date (YYYY-MM-DD)");
|
|
27
|
+
expression = dateLiteral(candidate);
|
|
28
|
+
cursor = 10;
|
|
29
|
+
fixedAnchor = true;
|
|
30
|
+
}
|
|
31
|
+
if (fixedAnchor && cursor < input.length) {
|
|
32
|
+
if (input.slice(cursor, cursor + 2) !== "||") return yield* failAt(input, cursor, "\"||\" before date operations");
|
|
33
|
+
cursor += 2;
|
|
34
|
+
if (cursor === input.length) return yield* failAt(input, cursor, "an operation beginning with \"+\", \"-\", or \"/\"");
|
|
35
|
+
}
|
|
36
|
+
while (cursor < input.length) {
|
|
37
|
+
const operator = input[cursor];
|
|
38
|
+
if (operator === "/") {
|
|
39
|
+
const unit = unitFromSymbol(input[cursor + 1] ?? "");
|
|
40
|
+
if (unit === void 0) return yield* failAt(input, cursor + 1, "a date unit: d, w, M, q, or y");
|
|
41
|
+
expression = startOf(expression, unit);
|
|
42
|
+
cursor += 2;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (operator !== "+" && operator !== "-") return yield* failAt(input, cursor, "an operation beginning with \"+\", \"-\", or \"/\"");
|
|
46
|
+
const amountStart = cursor + 1;
|
|
47
|
+
if (!isFirstPositiveDigit(input[amountStart] ?? "")) return yield* failAt(input, amountStart, "a positive integer without a leading zero");
|
|
48
|
+
cursor = amountStart + 1;
|
|
49
|
+
while (cursor < input.length && isDigit(input[cursor] ?? "")) cursor += 1;
|
|
50
|
+
const amount = Number(input.slice(amountStart, cursor));
|
|
51
|
+
if (!Number.isSafeInteger(amount)) return yield* failAt(input, amountStart, "a safe positive integer");
|
|
52
|
+
const unit = unitFromSymbol(input[cursor] ?? "");
|
|
53
|
+
if (unit === void 0) return yield* failAt(input, cursor, "a date unit: d, w, M, q, or y");
|
|
54
|
+
expression = shift(expression, operator === "+" ? amount : -amount, unit);
|
|
55
|
+
cursor += 1;
|
|
56
|
+
}
|
|
57
|
+
return expression;
|
|
58
|
+
});
|
|
59
|
+
const appendOperation = (base, operation) => ({
|
|
60
|
+
text: `${base.text}${base.fixedAnchor ? "||" : ""}${operation}`,
|
|
61
|
+
fixedAnchor: false
|
|
62
|
+
});
|
|
63
|
+
const formatInstantExpression = (expression) => foldInstant(normalizeInstant(expression), {
|
|
64
|
+
now: () => ({
|
|
65
|
+
text: "now",
|
|
66
|
+
fixedAnchor: false
|
|
67
|
+
}),
|
|
68
|
+
dateLiteral: (value) => ({
|
|
69
|
+
text: value,
|
|
70
|
+
fixedAnchor: true
|
|
71
|
+
}),
|
|
72
|
+
shift: (base, amount, unit) => appendOperation(base, `${amount < 0 ? "-" : "+"}${Math.abs(amount)}${symbolFromUnit(unit)}`),
|
|
73
|
+
startOf: (base, unit) => appendOperation(base, `/${symbolFromUnit(unit)}`)
|
|
74
|
+
}).text;
|
|
75
|
+
//#endregion
|
|
76
|
+
export { formatInstantExpression, parseInstantExpression };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
//#region src/filter/schema.d.ts
|
|
3
|
+
declare const DateExpressionString: Schema.String;
|
|
4
|
+
type DateExpressionString = typeof DateExpressionString.Type;
|
|
5
|
+
declare const DateFilter: Schema.Struct<{
|
|
6
|
+
readonly gt: Schema.optionalKey<Schema.String>;
|
|
7
|
+
readonly gte: Schema.optionalKey<Schema.String>;
|
|
8
|
+
readonly lt: Schema.optionalKey<Schema.String>;
|
|
9
|
+
readonly lte: Schema.optionalKey<Schema.String>;
|
|
10
|
+
}>;
|
|
11
|
+
type DateFilter = typeof DateFilter.Type;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { DateExpressionString, DateFilter };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
//#region src/filter/schema.ts
|
|
3
|
+
const DateExpressionString = Schema.String.check(Schema.isMinLength(1)).annotate({ identifier: "DateExpressionString" });
|
|
4
|
+
const DateFilter = Schema.Struct({
|
|
5
|
+
gt: Schema.optionalKey(DateExpressionString),
|
|
6
|
+
gte: Schema.optionalKey(DateExpressionString),
|
|
7
|
+
lt: Schema.optionalKey(DateExpressionString),
|
|
8
|
+
lte: Schema.optionalKey(DateExpressionString)
|
|
9
|
+
}).check(Schema.makeFilter((filter) => !(filter.gt !== void 0 && filter.gte !== void 0), { expected: "at most one lower date bound" }), Schema.makeFilter((filter) => !(filter.lt !== void 0 && filter.lte !== void 0), { expected: "at most one upper date bound" }), Schema.makeFilter((filter) => filter.gt !== void 0 || filter.gte !== void 0 || filter.lt !== void 0 || filter.lte !== void 0, { expected: "at least one date bound" })).annotate({ identifier: "DateFilter" });
|
|
10
|
+
//#endregion
|
|
11
|
+
export { DateExpressionString, DateFilter };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { InstantExpr } from "../ast/schemas.mjs";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
//#region src/filter/transformation.d.ts
|
|
4
|
+
declare const InstantExpressionFromString: Schema.decodeTo<Schema.Codec<InstantExpr, InstantExpr, never, never>, Schema.String, never, never>;
|
|
5
|
+
declare const DateRangeFromFilter: Schema.decodeTo<Schema.Union<readonly [Schema.TaggedStruct<"DateRange", {
|
|
6
|
+
readonly lower: Schema.Union<readonly [Schema.TaggedStruct<"GreaterThan", {
|
|
7
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
8
|
+
}>, Schema.TaggedStruct<"GreaterThanOrEqual", {
|
|
9
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
10
|
+
}>]>;
|
|
11
|
+
readonly upper: Schema.Union<readonly [Schema.TaggedStruct<"LessThan", {
|
|
12
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
13
|
+
}>, Schema.TaggedStruct<"LessThanOrEqual", {
|
|
14
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
15
|
+
}>]>;
|
|
16
|
+
}>, Schema.TaggedStruct<"DateRange", {
|
|
17
|
+
readonly lower: Schema.Union<readonly [Schema.TaggedStruct<"GreaterThan", {
|
|
18
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
19
|
+
}>, Schema.TaggedStruct<"GreaterThanOrEqual", {
|
|
20
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
21
|
+
}>]>;
|
|
22
|
+
readonly upper: Schema.optionalKey<Schema.Never>;
|
|
23
|
+
}>, Schema.TaggedStruct<"DateRange", {
|
|
24
|
+
readonly lower: Schema.optionalKey<Schema.Never>;
|
|
25
|
+
readonly upper: Schema.Union<readonly [Schema.TaggedStruct<"LessThan", {
|
|
26
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
27
|
+
}>, Schema.TaggedStruct<"LessThanOrEqual", {
|
|
28
|
+
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|
|
29
|
+
}>]>;
|
|
30
|
+
}>]>, Schema.Struct<{
|
|
31
|
+
readonly gt: Schema.optionalKey<Schema.String>;
|
|
32
|
+
readonly gte: Schema.optionalKey<Schema.String>;
|
|
33
|
+
readonly lt: Schema.optionalKey<Schema.String>;
|
|
34
|
+
readonly lte: Schema.optionalKey<Schema.String>;
|
|
35
|
+
}>, never, never>;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { DateRangeFromFilter, InstantExpressionFromString };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { DateRangeExpr, InstantExpr } from "../ast/schemas.mjs";
|
|
2
|
+
import { formatInstantExpression, parseInstantExpression } from "./expression.mjs";
|
|
3
|
+
import { DateFilter } from "./schema.mjs";
|
|
4
|
+
import { formatFilter, parseFilter } from "./codec.mjs";
|
|
5
|
+
import { Effect, Match, Schema, SchemaIssue, SchemaTransformation } from "effect";
|
|
6
|
+
//#region src/filter/transformation.ts
|
|
7
|
+
const expressionIssue = (input, offset, expected) => new SchemaIssue.Forbidden({ message: `Invalid date expression at offset ${offset}: expected ${expected}; input: ${input}` });
|
|
8
|
+
const InstantExpressionFromString = Schema.String.pipe(Schema.decodeTo(InstantExpr, SchemaTransformation.transformOrFail({
|
|
9
|
+
decode: (input) => Effect.mapError(parseInstantExpression(input), (error) => expressionIssue(error.input, error.offset, error.expected)),
|
|
10
|
+
encode: (expression) => Effect.succeed(formatInstantExpression(expression))
|
|
11
|
+
})));
|
|
12
|
+
const DateRangeFromFilter = DateFilter.pipe(Schema.decodeTo(DateRangeExpr, SchemaTransformation.transformOrFail({
|
|
13
|
+
decode: (filter) => Effect.mapError(parseFilter(filter), (error) => new SchemaIssue.Forbidden({ message: Match.valueTags(error, {
|
|
14
|
+
FilterExpressionParseError: (parseError) => `Invalid filter expression at offset ${parseError.offset}: expected ${parseError.expected}`,
|
|
15
|
+
InvalidDateFilterError: (filterError) => filterError.message
|
|
16
|
+
}) })),
|
|
17
|
+
encode: (range) => Effect.succeed(formatFilter(range))
|
|
18
|
+
})));
|
|
19
|
+
//#endregion
|
|
20
|
+
export { DateRangeFromFilter, InstantExpressionFromString };
|