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
156 changes: 156 additions & 0 deletions src/expressions/functions/builtInFunctions_sequences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import ISequence from '../dataTypes/ISequence';
import isSubtypeOf from '../dataTypes/isSubtypeOf';
import sequenceFactory from '../dataTypes/sequenceFactory';
import Value, { SequenceMultiplicity, SequenceType, ValueType } from '../dataTypes/Value';
import DynamicContext from '../DynamicContext';
import ExecutionParameters from '../ExecutionParameters';
import { valueCompare } from '../operators/compares/ValueCompare';
import StaticContext from '../StaticContext';
import { BUILT_IN_NAMESPACE_URIS } from '../staticallyKnownNamespaces';
import { DONE_TOKEN, IIterator, IterationHint, ready } from '../util/iterators';
import zipSingleton from '../util/zipSingleton';
Expand Down Expand Up @@ -690,6 +693,105 @@ const fnFoldRight: FunctionDefinitionType = (
);
};

function compareKeys(keysA: Value[], keysB: Value[], dynamicContext: DynamicContext): number {
const maxLen = Math.max(keysA.length, keysB.length);
for (let i = 0; i < maxLen; i++) {
const a = i < keysA.length ? keysA[i] : null;
const b = i < keysB.length ? keysB[i] : null;

if (a === null && b === null) continue;
// Empty sequences sort first (least)
if (a === null) return -1;
if (b === null) return 1;

const aIsNaN = typeof a.value === 'number' && isNaN(a.value);
const bIsNaN = typeof b.value === 'number' && isNaN(b.value);

if (aIsNaN && bIsNaN) continue;
// NaN sorts last (greatest)
if (aIsNaN) return 1;
if (bIsNaN) return -1;

if (valueCompare('gtOp', a.type, b.type)(a, b, dynamicContext)) return 1;
if (valueCompare('ltOp', a.type, b.type)(a, b, dynamicContext)) return -1;
}
return 0;
}

function doSort(
dynamicContext: DynamicContext,
executionParameters: ExecutionParameters,
staticContext: StaticContext,
sequence: ISequence,
keyFn: FunctionValue | null,
): ISequence {
return sequence.mapAll((items) => {
if (items.length === 0) {
return sequenceFactory.empty();
}

// Compute raw sort keys (sequence of atomic values) for each item
const rawKeys: Value[][] = items.map((item) => {
if (keyFn) {
const argTypes = keyFn.getArgumentTypes();
const arg = performFunctionConversion(
argTypes[0] as SequenceType,
sequenceFactory.singleton(item),
executionParameters,
'fn:sort',
false,
);
return keyFn.value
.call(undefined, dynamicContext, executionParameters, staticContext, arg)
.getAllValues();
}
return atomize(sequenceFactory.singleton(item), executionParameters).getAllValues();
});

// Cast xs:untypedAtomic to xs:string per spec
const castKeys: Value[][] = rawKeys.map((keys) =>
keys.map((key) =>
isSubtypeOf(key.type, ValueType.XSUNTYPEDATOMIC)
? castToType(key, ValueType.XSSTRING)
: key,
),
);

// For each key position, convert all values at that position to a common type
const maxKeyLen = castKeys.reduce((max, k) => Math.max(max, k.length), 0);
const finalKeys: Value[][] = castKeys.map((k) => [...k]);

for (let pos = 0; pos < maxKeyLen; pos++) {
const entries: { itemIndex: number; value: Value }[] = [];
castKeys.forEach((keys, itemIndex) => {
if (keys.length > pos) {
entries.push({ itemIndex, value: keys[pos] });
}
});

const converted = convertItemsToCommonType(entries.map((e) => e.value));
if (!converted) {
throw new Error('XPTY0004: fn:sort key values must be comparable');
}

entries.forEach(({ itemIndex }, idx) => {
finalKeys[itemIndex][pos] = converted[idx];
});
}

const sortable = items.map((item, i) => ({ item, keys: finalKeys[i] }));
sortable.sort((a, b) => compareKeys(a.keys, b.keys, dynamicContext));
return sequenceFactory.create(sortable.map((s) => s.item));
});
}

const fnSort: FunctionDefinitionType = (
dynamicContext,
executionParameters,
staticContext,
sequence,
) => doSort(dynamicContext, executionParameters, staticContext, sequence, null);

const fnSerialize: FunctionDefinitionType = (
_dynamicContext,
executionParameters,
Expand Down Expand Up @@ -1086,6 +1188,60 @@ const declarations: BuiltinDeclarationType[] = [
namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
returnType: { type: ValueType.XSSTRING, mult: SequenceMultiplicity.EXACTLY_ONE },
},

{
argumentTypes: [{ type: ValueType.ITEM, mult: SequenceMultiplicity.ZERO_OR_MORE }],
callFunction: fnSort,
localName: 'sort',
namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
returnType: { type: ValueType.ITEM, mult: SequenceMultiplicity.ZERO_OR_MORE },
},

{
argumentTypes: [
{ type: ValueType.ITEM, mult: SequenceMultiplicity.ZERO_OR_MORE },
{ type: ValueType.XSSTRING, mult: SequenceMultiplicity.ZERO_OR_ONE },
],
callFunction: (dynamicContext, executionParameters, staticContext, sequence, collation) => {
if (!collation.isEmpty()) {
throw new Error('FOCH0002: No collations are supported');
}
return doSort(dynamicContext, executionParameters, staticContext, sequence, null);
},
localName: 'sort',
namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
returnType: { type: ValueType.ITEM, mult: SequenceMultiplicity.ZERO_OR_MORE },
},

{
argumentTypes: [
{ type: ValueType.ITEM, mult: SequenceMultiplicity.ZERO_OR_MORE },
{ type: ValueType.XSSTRING, mult: SequenceMultiplicity.ZERO_OR_ONE },
{ type: ValueType.FUNCTION, mult: SequenceMultiplicity.EXACTLY_ONE },
],
callFunction: (
dynamicContext,
executionParameters,
staticContext,
sequence,
collation,
keySeq,
) => {
if (!collation.isEmpty()) {
throw new Error('FOCH0002: No collations are supported');
}
return doSort(
dynamicContext,
executionParameters,
staticContext,
sequence,
keySeq.first() as FunctionValue,
);
},
localName: 'sort',
namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
returnType: { type: ValueType.ITEM, mult: SequenceMultiplicity.ZERO_OR_MORE },
},
];

export default {
Expand Down
2 changes: 1 addition & 1 deletion test/assets/runnableTestSets.csv
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ fn-seconds-from-duration,true
fn-seconds-from-time,true
fn-serialize,true
fn-serialize-json,false
fn-sort,false
fn-sort,true
fn-starts-with,true
fn-static-base-uri,false
fn-string,true
Expand Down
Loading
Loading