Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 33 additions & 23 deletions src/elm/query.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Context } from '../runtime/context';
import { equals, greaterThan, lessThan } from '../util/comparison';
import { allTrue, asyncMergeSort, Direction, typeIsArray } from '../util/util';
import { build } from './builder';
import { Expression, UnimplementedExpression } from './expression';
Expand Down Expand Up @@ -62,6 +63,36 @@ export class Without extends With {
// ELM-only, not a product of CQL
export class Sort extends UnimplementedExpression {}

function order(a: any, b: any, low_order: number, high_order: number): number {
if (a == null || b == null) {
if (a == null && b == null) {
return 0;
}
// "When the data being sorted includes nulls, they are sorted first, meaning they will appear at the beginning of the list when the data is sorted ascending,
// and at the end of the list when the data is sorted descending."
return a == null ? low_order : high_order;
} else if (equals(a, b)) {
return 0;
} else if (lessThan(a, b)) {
return low_order;
} else if (greaterThan(a, b)) {
return high_order;
} else {
// Indeterminate, for example comparing DateTimes of different precision.
// The spec doesn't clearly say what to do here (see https://github.com/cqframework/cql-tests/issues/168 ) so the heuristics below are an implementation decision.

if (typeof a.getPrecisionValue === 'function' && typeof b.getPrecisionValue === 'function') {
// Order DateTimes with lower precision first.
// Comparison functions above internally convert DateTimes to the same timezone, so if we get here, we know any timezone difference doesn't impact the result
return order(a.getPrecisionValue(), b.getPrecisionValue(), low_order, high_order);
}

// Fallback to the plain JS operators, which coerce objects to primitives, likely strings: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Less_than
// This is imperfect but at least sorts symmetrically (a < b is always true or false, and a < b always implies b > a)
return a < b ? low_order : high_order;
}
}

export class ByDirection extends Expression {
direction: Direction;
low_order: number;
Expand All @@ -76,19 +107,7 @@ export class ByDirection extends Expression {

// @ts-ignore
async exec(ctx: Context, a: any, b: any) {
if (a === b) {
return 0;
} else if (a.isQuantity && b.isQuantity) {
if (a.before(b)) {
return this.low_order;
} else {
return this.high_order;
}
} else if (a < b) {
return this.low_order;
} else {
return this.high_order;
}
return order(a, b, this.low_order, this.high_order);
}
}

Expand All @@ -112,16 +131,7 @@ export class ByExpression extends Expression {
const a_val = await this.expression.execute(sctx);
sctx = ctx.childContext(b);
const b_val = await this.expression.execute(sctx);

if (a_val === b_val || (a_val == null && b_val == null)) {
return 0;
} else if (a_val == null || b_val == null) {
return a_val == null ? this.low_order : this.high_order;
} else if (a_val.isQuantity && b_val.isQuantity) {
return a_val.before(b_val) ? this.low_order : this.high_order;
} else {
return a_val < b_val ? this.low_order : this.high_order;
}
return order(a_val, b_val, this.low_order, this.high_order);
}
}

Expand Down
33 changes: 33 additions & 0 deletions test/elm/query/data.cql
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,39 @@ define sortByExpressionWithNullResults: ({8, null, 7}) N return Tuple{N: N} sort
define sortWithAscendingKeyword: ({8, 6, 7, 5, 3, 0, 9}) N sort ascending
define sortWithDescendingKeyword: ({8, 6, 7, 5, 3, 0, 9}) N return N sort descending

define Newest: Last(({ Tuple { id: 'older 05:00Z', d: @2025-01-02T05:00:00.000Z }, Tuple { id: 'newer 06:00Z', d: @2025-01-01T20:00:00.000-10:00 } }) X sort by d).id
define NewestReversedInput: Last(({ Tuple { id: 'newer 06:00Z', d: @2025-01-01T20:00:00.000-10:00 }, Tuple { id: 'older 05:00Z', d: @2025-01-02T05:00:00.000Z } }) X sort by d).id
define DateTimeDirectionAsc: ({ @2025-01-02T05:00:00.000Z, @2025-01-01T20:00:00.000-10:00 }) X sort asc
define DateTimeDirectionDesc: ({ @2025-01-02T05:00:00.000Z, @2025-01-01T20:00:00.000-10:00 }) X sort desc
define DateTimeColumnAsc: ({ Tuple { value: @2025-01-02T05:00:00.000Z }, Tuple { value: @2025-01-01T20:00:00.000-10:00 } }) X sort by value asc
define DateTimeColumnDesc: ({ Tuple { value: @2025-01-02T05:00:00.000Z }, Tuple { value: @2025-01-01T20:00:00.000-10:00 } }) X sort by value desc
define DateTimeExpressionAsc: ({ Tuple { value: @2025-01-02T05:00:00.000Z }, Tuple { value: @2025-01-01T20:00:00.000-10:00 } }) X sort by (value + 0 days) asc
define DateTimeExpressionDesc: ({ Tuple { value: @2025-01-02T05:00:00.000Z }, Tuple { value: @2025-01-01T20:00:00.000-10:00 } }) X sort by (value + 0 days) desc
define DateTimeDirectionAscReversed: ({ @2025-01-01T20:00:00.000-10:00, @2025-01-02T05:00:00.000Z }) X sort asc
define DateTimeDirectionDescReversed: ({ @2025-01-01T20:00:00.000-10:00, @2025-01-02T05:00:00.000Z }) X sort desc
define DateTimeColumnAscReversed: ({ Tuple { value: @2025-01-01T20:00:00.000-10:00 }, Tuple { value: @2025-01-02T05:00:00.000Z } }) X sort by value asc
define DateTimeColumnDescReversed: ({ Tuple { value: @2025-01-01T20:00:00.000-10:00 }, Tuple { value: @2025-01-02T05:00:00.000Z } }) X sort by value desc
define DateTimeExpressionAscReversed: ({ Tuple { value: @2025-01-01T20:00:00.000-10:00 }, Tuple { value: @2025-01-02T05:00:00.000Z } }) X sort by (value + 0 days) asc
define DateTimeExpressionDescReversed: ({ Tuple { value: @2025-01-01T20:00:00.000-10:00 }, Tuple { value: @2025-01-02T05:00:00.000Z } }) X sort by (value + 0 days) desc
// The hour-precision value spans 07:00Z–07:59:59.999Z, entirely after 05:30Z.
define DateTimeMixedPrecisionAsc: ({ @2025-01-02T05:30:00.000Z, @2025-01-02T05-02:00 }) X sort asc
define DateTimeMixedPrecisionAscReversed: ({ @2025-01-02T05-02:00, @2025-01-02T05:30:00.000Z }) X sort asc
// 05-02:00 spans 07:00Z–07:59:59.999Z and overlaps the precise 07:30Z instant.
define DateTimeIndeterminatePrecisionAsc: ({ @2025-01-02T07:30:00.000Z, @2025-01-02T05-02:00 }) X sort asc
define DateTimeIndeterminatePrecisionAscReversed: ({ @2025-01-02T05-02:00, @2025-01-02T07:30:00.000Z }) X sort asc
define DecimalDirectionAsc: ({ 2.5, 10.25 }) X sort asc
define DecimalDirectionDesc: ({ 2.5, 10.25 }) X sort desc
define DecimalColumnAsc: ({ Tuple { value: 2.5 }, Tuple { value: 10.25 } }) X sort by value asc
define DecimalColumnDesc: ({ Tuple { value: 2.5 }, Tuple { value: 10.25 } }) X sort by value desc
define DecimalExpressionAsc: ({ Tuple { value: 2.5 }, Tuple { value: 10.25 } }) X sort by (value + 0.0) asc
define DecimalExpressionDesc: ({ Tuple { value: 2.5 }, Tuple { value: 10.25 } }) X sort by (value + 0.0) desc
define DecimalDirectionAscReversed: ({ 10.25, 2.5 }) X sort asc
define DecimalDirectionDescReversed: ({ 10.25, 2.5 }) X sort desc
define DecimalColumnAscReversed: ({ Tuple { value: 10.25 }, Tuple { value: 2.5 } }) X sort by value asc
define DecimalColumnDescReversed: ({ Tuple { value: 10.25 }, Tuple { value: 2.5 } }) X sort by value desc
define DecimalExpressionAscReversed: ({ Tuple { value: 10.25 }, Tuple { value: 2.5 } }) X sort by (value + 0.0) asc
define DecimalExpressionDescReversed: ({ Tuple { value: 10.25 }, Tuple { value: 2.5 } }) X sort by (value + 0.0) desc

// @Test: Distinct
define defaultNumbers: ({1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 3, 3, 3, 2, 2, 1}) N return N
define defaultStrings: ({'foo', 'bar', 'baz', 'bar'}) S return S
Expand Down
Loading
Loading