Skip to content

Commit

Permalink
Quite a few date/time fixes
Browse files Browse the repository at this point in the history
Generally defer to `number` function (overridden in xforms function library) for date/datetime-like values in argument positions and numeric comparisons
  • Loading branch information
eyelidlessness committed Oct 16, 2023
1 parent 663670a commit f3c28f8
Show file tree
Hide file tree
Showing 27 changed files with 356 additions and 204 deletions.
2 changes: 1 addition & 1 deletion packages/odk-xpath/src/context/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ export interface Context {
// TODO: namespaced function libraries? Could accommodate custom functions
readonly functionLibrary: FunctionLibrary;
readonly namespaceResolver: XPathNamespaceResolverObject;
readonly timeZone: Temporal.TimeZoneProtocol;
readonly timeZone: Temporal.TimeZone;
}
4 changes: 2 additions & 2 deletions packages/odk-xpath/src/context/EvaluationContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export interface EvaluationContextOptions {
readonly rootNode: ContextParentNode;
readonly functionLibrary: FunctionLibrary;
readonly namespaceResolver: XPathNamespaceResolverObject;
readonly timeZone: Temporal.TimeZoneProtocol;
readonly timeZone: Temporal.TimeZone;
readonly treeWalkers: EvaluationContextTreeWalkers;
}

Expand All @@ -53,7 +53,7 @@ export class EvaluationContext implements Context {
readonly functionLibrary: FunctionLibrary;
readonly namespaceResolver: XPathNamespaceResolverObject;

readonly timeZone: Temporal.TimeZoneProtocol;
readonly timeZone: Temporal.TimeZone;

readonly treeWalkers: EvaluationContextTreeWalkers;

Expand Down
3 changes: 2 additions & 1 deletion packages/odk-xpath/src/evaluations/BooleanEvaluation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { LocationPathEvaluation } from './LocationPathEvaluation.ts';
import { ValueEvaluation } from './ValueEvaluation.ts';

export class BooleanEvaluation extends ValueEvaluation<'BOOLEAN'> {
Expand All @@ -8,7 +9,7 @@ export class BooleanEvaluation extends ValueEvaluation<'BOOLEAN'> {
protected readonly numberValue: number;
protected readonly stringValue: string;

constructor(readonly value: boolean) {
constructor(readonly context: LocationPathEvaluation, readonly value: boolean) {
super();

this.booleanValue = value;
Expand Down
43 changes: 36 additions & 7 deletions packages/odk-xpath/src/evaluations/DateTimeLikeEvaluation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { Temporal } from '@js-temporal/polyfill';
import { DAY_MILLISECONDS, DateTimeContext } from '../lib/datetime';
import { DAY_MILLISECONDS } from '../lib/datetime/constants.ts';
import type { LocationPathEvaluation } from './LocationPathEvaluation.ts';
import { ValueEvaluation } from './ValueEvaluation.ts';
import { localDateTimeOrDateString } from '../lib/datetime/functions.ts';

interface PrecomputedXPathValues {
readonly booleanValue?: boolean;
readonly numberValue?: number;
readonly stringValue?: string;
}

const INVALID_DATE_TIME_STRING = 'Invalid Date';

export class DateTimeLikeEvaluation extends ValueEvaluation<'NUMBER'> {
readonly type = 'NUMBER';
Expand All @@ -16,23 +26,42 @@ export class DateTimeLikeEvaluation extends ValueEvaluation<'NUMBER'> {
protected readonly dateTimeString: string;

constructor(
protected readonly context: DateTimeContext,
protected dateTime: Temporal.ZonedDateTime
readonly context: LocationPathEvaluation,
protected dateTime: Temporal.ZonedDateTime | null,
precomputedValues: PrecomputedXPathValues = {}
) {
super();

const {
booleanValue,
numberValue,
stringValue,
} = precomputedValues;

if (dateTime == null) {
this.value = NaN;
this.booleanValue = booleanValue ?? false;
this.numberValue = numberValue ?? NaN;
this.milliseconds = NaN;
this.dateString = INVALID_DATE_TIME_STRING;
this.dateTimeString = INVALID_DATE_TIME_STRING;
this.stringValue = stringValue ?? INVALID_DATE_TIME_STRING;

return;
}

const { epochMilliseconds } = dateTime;

this.value = epochMilliseconds;

this.booleanValue = epochMilliseconds !== 0;
this.numberValue = epochMilliseconds / DAY_MILLISECONDS;
this.booleanValue = booleanValue ?? (epochMilliseconds !== 0);
this.numberValue = numberValue ?? (epochMilliseconds / DAY_MILLISECONDS);

const dateTimeString = context.toDateOrDateTimeString(dateTime);
const dateTimeString = localDateTimeOrDateString(dateTime);

this.milliseconds = epochMilliseconds;
this.dateTimeString = dateTimeString;
this.stringValue = dateTimeString.replace(/T00:00:00$/, '');
this.stringValue = stringValue ?? dateTimeString;
this.dateString = dateTimeString.replace(/T.*$/, '');
}
}
3 changes: 3 additions & 0 deletions packages/odk-xpath/src/evaluations/Evaluation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { EvaluationType } from './EvaluationType.ts';
import type { LocationPathEvaluation } from './LocationPathEvaluation.ts';

export interface Evaluation<Type extends EvaluationType = EvaluationType>
extends Iterable<Evaluation<Type>> {

readonly context: LocationPathEvaluation;
readonly type: Type;

first(): Evaluation<Type> | null;
Expand Down
15 changes: 11 additions & 4 deletions packages/odk-xpath/src/evaluations/LocationPathEvaluation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@ import { NodeEvaluation } from './NodeEvaluation.ts';

type LocationPathParentContext = EvaluationContext | LocationPathEvaluation;

const mapNodeEvaluations = map((node: Node) => new NodeEvaluation(node));
function* toNodeEvaluations(
context: LocationPathEvaluation,
nodes: Iterable<ContextNode>
): Iterable<NodeEvaluation> {
for (const node of nodes) {
yield new NodeEvaluation(context, node);
}
}

type EvaluationComparator = (lhs: Evaluation, rhs: Evaluation) => boolean;

Expand Down Expand Up @@ -106,7 +113,7 @@ export class LocationPathEvaluation

// --- Context ---
readonly evaluator: Evaluator;

readonly context: LocationPathEvaluation = this;
readonly contextDocument: ContextDocument;
readonly rootNode: ContextParentNode;

Expand All @@ -130,7 +137,7 @@ export class LocationPathEvaluation

readonly treeWalkers: EvaluationContextTreeWalkers;

readonly timeZone: Temporal.TimeZoneProtocol;
readonly timeZone: Temporal.TimeZone;

/**
* TODO: this is a temporary accommodation for these cases which are presently
Expand Down Expand Up @@ -198,7 +205,7 @@ export class LocationPathEvaluation

this.nodes = nodes;

this.nodeEvaluations = Reiterable.from(mapNodeEvaluations(contextNodes));
this.nodeEvaluations = Reiterable.from(toNodeEvaluations(this, contextNodes));

if (options.contextSize != null) {
this.optionsContextSize = options.contextSize;
Expand Down
23 changes: 20 additions & 3 deletions packages/odk-xpath/src/evaluations/NodeEvaluation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { trimXPathWhitespace } from '../lib/strings/xpath-whitespace.ts';
import type { LocationPathEvaluation } from './LocationPathEvaluation.ts';
import { StringEvaluation } from './StringEvaluation.ts';
import { ValueEvaluation } from './ValueEvaluation.ts';

interface NodeEvaluationComputedValues {
Expand Down Expand Up @@ -30,7 +32,7 @@ export class NodeEvaluation extends ValueEvaluation<'NODE'> {
return this.computeValues().isEmpty;
}

constructor(readonly value: Node) {
constructor(readonly context: LocationPathEvaluation, readonly value: Node) {
super();
this.nodes = [value];
}
Expand All @@ -39,14 +41,29 @@ export class NodeEvaluation extends ValueEvaluation<'NODE'> {
let { computedValues } = this;

if (computedValues == null) {
const { value: node } = this;
const { context, value: node } = this;
const stringValue = node.textContent ?? '';
const isEmpty = trimXPathWhitespace(stringValue) === '';
const booleanValue = !isEmpty;
const numberFunction = context.functionLibrary.getImplementation('number');

let numberValue: number;

// Note: without this `isEmpty` check, `Number('')` would produce 0.
// Which is wrong! Thanks, Netscape!
const numberValue = isEmpty ? NaN : Number(stringValue);
if (isEmpty) {
numberValue = NaN;
} else if (numberFunction == null) {
numberValue = Number(stringValue);
} else {
const stringEvaluation = new StringEvaluation(context, stringValue);

numberValue = numberFunction.call(context, [
{
evaluate: () => stringEvaluation,
}
]).toNumber();
}

computedValues = {
booleanValue,
Expand Down
3 changes: 2 additions & 1 deletion packages/odk-xpath/src/evaluations/NumberEvaluation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { LocationPathEvaluation } from './LocationPathEvaluation.ts';
import { ValueEvaluation } from './ValueEvaluation.ts';

export class NumberEvaluation extends ValueEvaluation<'NUMBER'> {
Expand All @@ -8,7 +9,7 @@ export class NumberEvaluation extends ValueEvaluation<'NUMBER'> {
protected readonly numberValue: number;
protected readonly stringValue: string;

constructor(readonly value: number) {
constructor(readonly context: LocationPathEvaluation, readonly value: number) {
super();

this.booleanValue = value !== 0 && !Number.isNaN(value);
Expand Down
18 changes: 18 additions & 0 deletions packages/odk-xpath/src/evaluations/StringEvaluation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { LocationPathEvaluation } from './LocationPathEvaluation.ts';
import { ValueEvaluation } from './ValueEvaluation.ts';

export class StringEvaluation extends ValueEvaluation<'STRING'> {
Expand All @@ -9,6 +10,7 @@ export class StringEvaluation extends ValueEvaluation<'STRING'> {
protected readonly stringValue: string;

constructor(
readonly context: LocationPathEvaluation,
readonly value: string,
readonly isEmpty: boolean = value === ''
) {
Expand All @@ -17,5 +19,21 @@ export class StringEvaluation extends ValueEvaluation<'STRING'> {
this.booleanValue = !isEmpty;
this.numberValue = isEmpty ? NaN : Number(value);
this.stringValue = value;

const numberFunction = context.functionLibrary.getImplementation('number');

if (isEmpty) {
this.numberValue = NaN;
} else {
this.numberValue = Number(value);

if (numberFunction != null) {
this.numberValue = numberFunction.call(context, [
{
evaluate: () => this,
}
]).toNumber();
}
}
}
}
2 changes: 2 additions & 0 deletions packages/odk-xpath/src/evaluations/ValueEvaluation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type { EvaluationType, EvaluationTypes } from './EvaluationType.ts';
import { LocationPathEvaluation } from './LocationPathEvaluation.ts';

export abstract class ValueEvaluation<Type extends EvaluationType> implements Evaluation<Type> {
abstract readonly context: LocationPathEvaluation;

abstract readonly type: Type;
abstract readonly value: EvaluationTypes[Type];
abstract readonly nodes: Type extends 'NODE' ? Iterable<Node> : null;
Expand Down
13 changes: 7 additions & 6 deletions packages/odk-xpath/src/evaluator/Evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { EvaluationContextOptions } from '../context/EvaluationContext.ts';
import { EvaluationContext } from '../context/EvaluationContext.ts';
import { fn } from '../functions/index.ts';
import type { ContextNode } from '../lib/dom/types.ts';
import type { ParseOptions } from '../static/grammar/ExpressionParser.ts';
import { ExpressionParser } from '../static/grammar/ExpressionParser.ts';
import { createExpression } from './expression/Expression.ts';
import { FunctionLibrary } from './functions/FunctionLibrary.ts';
Expand All @@ -24,21 +25,21 @@ const parser = new ExpressionParser();
// }

interface EvaluatorOptions {
readonly parseOptions?: ParseOptions;
readonly functionLibrary?: FunctionLibrary;
readonly timeZoneId?: string | undefined;
}

export class Evaluator implements XPathEvaluator {
readonly functionLibrary: FunctionLibrary;
readonly parseOptions: ParseOptions;
readonly resultTypes: ResultTypes = ResultTypes;
readonly timeZone: Temporal.TimeZoneProtocol;
readonly timeZone: Temporal.TimeZone;

constructor(options: EvaluatorOptions = {}) {
this.functionLibrary = options.functionLibrary ?? fn;

const timeZoneId = options.timeZoneId ?? Temporal.Now.timeZoneId();

this.timeZone = Temporal.TimeZone.from(timeZoneId);
this.parseOptions = options.parseOptions ?? {};
this.timeZone = new Temporal.TimeZone(options.timeZoneId ?? Temporal.Now.timeZoneId());
}

evaluate(
Expand All @@ -47,7 +48,7 @@ export class Evaluator implements XPathEvaluator {
namespaceResolver: XPathNSResolver | null,
resultType: XPathResultType | null
) {
const tree = parser.parse(expression);
const tree = parser.parse(expression, this.parseOptions);

let contextOptions: Partial<EvaluationContextOptions> = {};

Expand Down
Loading

0 comments on commit f3c28f8

Please sign in to comment.