From b1d1ddef44652f3cde8b42501b68bae16d841963 Mon Sep 17 00:00:00 2001 From: latin-panda <66472237+latin-panda@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:21:02 +0200 Subject: [PATCH 1/4] fix NaN issue in coalesce --- packages/xpath/src/functions/xforms/string.ts | 8 +++++--- packages/xpath/test/xforms/coalesce.test.ts | 8 ++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/xpath/src/functions/xforms/string.ts b/packages/xpath/src/functions/xforms/string.ts index 84cc75c61..f8a1d6c0b 100644 --- a/packages/xpath/src/functions/xforms/string.ts +++ b/packages/xpath/src/functions/xforms/string.ts @@ -33,10 +33,12 @@ export const coalesce = new StringFunction( { arityType: 'required', typeHint: 'string' }, ], (context, [aExpression, bExpression]): string => { - const a = aExpression!.evaluate(context).toString(); + const aEval = aExpression!.evaluate(context); + const aValue = aEval.toString(); - if (a !== '') { - return a; + const isEmpty = aValue === '' || (aEval.type === 'NUMBER' && Number.isNaN(aEval.toNumber())); + if (!isEmpty) { + return aValue; } return bExpression!.evaluate(context).toString(); diff --git a/packages/xpath/test/xforms/coalesce.test.ts b/packages/xpath/test/xforms/coalesce.test.ts index c355342b3..11d71b583 100644 --- a/packages/xpath/test/xforms/coalesce.test.ts +++ b/packages/xpath/test/xforms/coalesce.test.ts @@ -40,6 +40,14 @@ describe('#coalesce()', () => { testContext.assertStringValue('coalesce(/simple/xpath/to/node, "SECOND")', 'SECOND'); }); + it('should return second value if first value is NaN', () => { + testContext.assertStringValue('coalesce(1 * /simple/xpath/to/node, "0")', '0'); + testContext.assertStringValue( + 'coalesce(/simple/xpath/to/node * /simple/xpath/to/node, "0")', + '0' + ); + }); + it('coalesce(self::*)', () => { testContext = createXFormsTestContext(`
From 912e6cea7a4ea3b3e660055679bf121b847597dc Mon Sep 17 00:00:00 2001 From: latin-panda <66472237+latin-panda@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:27:28 +0200 Subject: [PATCH 2/4] changeset --- .changeset/yummy-meals-care.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/yummy-meals-care.md diff --git a/.changeset/yummy-meals-care.md b/.changeset/yummy-meals-care.md new file mode 100644 index 000000000..ca7f1bc40 --- /dev/null +++ b/.changeset/yummy-meals-care.md @@ -0,0 +1,5 @@ +--- +'@getodk/xpath': patch +--- + +Fixes coalesce function to handle NaN From 7c2ab378d762057129dc5d9c12bbf7b38dee5ad0 Mon Sep 17 00:00:00 2001 From: latin-panda <66472237+latin-panda@users.noreply.github.com> Date: Wed, 8 Apr 2026 20:24:19 +0200 Subject: [PATCH 3/4] refactor --- packages/xpath/src/evaluations/NumberEvaluation.ts | 2 +- packages/xpath/src/functions/xforms/string.ts | 8 +++----- packages/xpath/test/xforms/once.test.ts | 4 ++-- packages/xpath/test/xforms/randomize.test.ts | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/xpath/src/evaluations/NumberEvaluation.ts b/packages/xpath/src/evaluations/NumberEvaluation.ts index ea380951d..bc1ae43a3 100644 --- a/packages/xpath/src/evaluations/NumberEvaluation.ts +++ b/packages/xpath/src/evaluations/NumberEvaluation.ts @@ -18,6 +18,6 @@ export class NumberEvaluation extends ValueEvaluation { - const aEval = aExpression!.evaluate(context); - const aValue = aEval.toString(); + const a = aExpression!.evaluate(context).toString(); - const isEmpty = aValue === '' || (aEval.type === 'NUMBER' && Number.isNaN(aEval.toNumber())); - if (!isEmpty) { - return aValue; + if (a !== '') { + return a; } return bExpression!.evaluate(context).toString(); diff --git a/packages/xpath/test/xforms/once.test.ts b/packages/xpath/test/xforms/once.test.ts index 0b730561f..6308fa682 100644 --- a/packages/xpath/test/xforms/once.test.ts +++ b/packages/xpath/test/xforms/once.test.ts @@ -27,8 +27,8 @@ describe('once()', () => { }); }); - it('should set value to NaN', () => { - testContext.assertStringValue('once(. * 10)', 'NaN', { + it('should set value to empty string when arithmetic on empty node produces NaN', () => { + testContext.assertStringValue('once(. * 10)', '', { contextNode, }); }); diff --git a/packages/xpath/test/xforms/randomize.test.ts b/packages/xpath/test/xforms/randomize.test.ts index a45b74877..1fe2a43e3 100644 --- a/packages/xpath/test/xforms/randomize.test.ts +++ b/packages/xpath/test/xforms/randomize.test.ts @@ -80,7 +80,7 @@ describe('randomize()', () => { { seed: 0, expected: 'CBEAFD' }, { seed: NaN, expected: 'CBEAFD' }, { seed: Infinity, expected: 'CBEAFD' }, - { seed: -Infinity, expected: 'CFBEAD' }, + { seed: -Infinity, expected: 'CBEAFD' }, { seed: 'floor(1.1)', expected: 'BFEACD' }, { seed: '//xhtml:div[@id="testFunctionNodeset2"]/xhtml:p', expected: 'BFEACD' }, { seed: MIRROR_HASH_VALUE, expected: MIRROR_HASH_SORT_ORDER }, From 6a77007507e9bd084e56f2387589593b8ab2e9d9 Mon Sep 17 00:00:00 2001 From: latin-panda <66472237+latin-panda@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:01:56 +0200 Subject: [PATCH 4/4] fixes tests --- .changeset/yummy-meals-care.md | 2 +- packages/scenario/test/instance-input.test.ts | 2 +- packages/xforms-engine/test/instance/instance.test.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.changeset/yummy-meals-care.md b/.changeset/yummy-meals-care.md index ca7f1bc40..9b57d66b0 100644 --- a/.changeset/yummy-meals-care.md +++ b/.changeset/yummy-meals-care.md @@ -2,4 +2,4 @@ '@getodk/xpath': patch --- -Fixes coalesce function to handle NaN +Stringify NaN as an empty string according to the XForms specs. diff --git a/packages/scenario/test/instance-input.test.ts b/packages/scenario/test/instance-input.test.ts index 260b313f6..dca4aa90b 100644 --- a/packages/scenario/test/instance-input.test.ts +++ b/packages/scenario/test/instance-input.test.ts @@ -812,7 +812,7 @@ describe.each([ readonly inner3: ComparableAnswer; } - const NAN_ANSWER = stringAnswer(String(NaN)); + const NAN_ANSWER = stringAnswer(''); interface MissingRepeatInstanceInputCase { readonly detail: string; diff --git a/packages/xforms-engine/test/instance/instance.test.ts b/packages/xforms-engine/test/instance/instance.test.ts index 717039949..c3f55893b 100644 --- a/packages/xforms-engine/test/instance/instance.test.ts +++ b/packages/xforms-engine/test/instance/instance.test.ts @@ -133,7 +133,7 @@ describe('Form instance state', () => { it.each([ { firstValue: '2', expected: '4' }, - { firstValue: '', expected: 'NaN' }, + { firstValue: '', expected: '' }, ])( 'updates the calculation to $expected when its dependency value is updated to $firstValue', ({ firstValue, expected }) => { @@ -331,7 +331,7 @@ describe('Form instance state', () => { it.each([ { firstValue: '2', expected: '6' }, { firstValue: '0', expected: '0' }, - { firstValue: '', expected: 'NaN' }, + { firstValue: '', expected: '' }, ])( 'updates the calculated value $expected while it is relevant', ({ firstValue, expected }) => { @@ -350,7 +350,7 @@ describe('Form instance state', () => { it.each([ { firstValue: '2', expected: '6' }, { firstValue: '0', expected: '0' }, - { firstValue: '', expected: 'NaN' }, + { firstValue: '', expected: '' }, ])( 'updates the calculated value $expected when it becomes relevant after the calculated dependency has been updated', ({ firstValue, expected }) => {