From 3a0c08caa81866427ba91780c3231c5f2477643e Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Tue, 13 Jan 2026 11:08:41 +0100 Subject: [PATCH 1/7] Tests for function VALUE --- test/unit/interpreter/function-value.spec.ts | 424 +++++++++++++++++++ 1 file changed, 424 insertions(+) create mode 100644 test/unit/interpreter/function-value.spec.ts diff --git a/test/unit/interpreter/function-value.spec.ts b/test/unit/interpreter/function-value.spec.ts new file mode 100644 index 000000000..05685444e --- /dev/null +++ b/test/unit/interpreter/function-value.spec.ts @@ -0,0 +1,424 @@ +import {CellValueDetailedType, ErrorType, HyperFormula} from '../../../src' +import {ErrorMessage} from '../../../src/error-message' +import {adr, detailedError} from '../testUtils' + +describe('Function VALUE', () => { + describe('argument validation', () => { + it('should return error for wrong number of arguments', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE()'], + ['=VALUE("1", "2")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.NA, ErrorMessage.WrongArgNumber)) + expect(engine.getCellValue(adr('A2'))).toEqualError(detailedError(ErrorType.NA, ErrorMessage.WrongArgNumber)) + }) + }) + + describe('basic numeric string conversion', () => { + it('should convert integer string', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("123")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toBe(123) + }) + + it('should convert decimal string', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("123.45")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toBe(123.45) + }) + + it('should convert negative number string', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("-123")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toBe(-123) + }) + + it('should convert string with plus sign', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("+123")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toBe(123) + }) + + it('should trim leading and trailing spaces', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE(" 123 ")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toBe(123) + }) + + it('should convert string with leading zeros', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("00123")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toBe(123) + }) + + it('should convert scientific notation (uppercase E)', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("1.23E3")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toBe(1230) + }) + + it('should convert scientific notation (lowercase e, negative exponent)', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("1.5e-2")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toBe(0.015) + }) + + it('should convert string with thousand separator', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("1,234")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toBe(1234) + }) + + it('should convert parentheses notation as negative number', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("(123)")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toBe(-123) + }) + }) + + describe('percentage strings', () => { + it('should convert percentage string to decimal', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("50%")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toBe(0.5) + }) + }) + + describe('currency strings', () => { + it('should convert currency string', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("$123")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toBe(123) + }) + + it('should convert currency string with thousand separator and decimal', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("$1,234.56")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toBe(1234.56) + }) + }) + + describe('date strings', () => { + it('should convert date string to serial number', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("01/13/2026")'], + ], {dateFormats: ['MM/DD/YYYY']}) + + expect(engine.getCellValue(adr('A1'))).toBe(46035) + }) + }) + + describe('time strings', () => { + it('should convert time string to fraction of day', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("14:30")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toBeCloseTo(0.60416667, 6) + }) + + it('should convert time string with seconds', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("12:30:45")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toBeCloseTo(0.52135417, 6) + }) + + it('should handle time greater than 24 hours', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("25:00")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toBeCloseTo(1.04166667, 6) + }) + }) + + describe('datetime strings', () => { + it('should convert datetime string to serial number with time fraction', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("01/13/2026 14:30")'], + ], {dateFormats: ['MM/DD/YYYY']}) + + expect(engine.getCellValue(adr('A1'))).toBeCloseTo(46035.60417, 4) + }) + }) + + describe('error cases', () => { + it('should return VALUE error for empty string', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.NumberCoercion)) + }) + + it('should return VALUE error for non-numeric string', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("abc")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.NumberCoercion)) + }) + + it('should return VALUE error for string with trailing text', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("123abc")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.NumberCoercion)) + }) + + it('should return VALUE error for European decimal format in default locale', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("123,45")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.NumberCoercion)) + }) + + it('should return VALUE error for 12-hour time format without proper config', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("3:00pm")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.NumberCoercion)) + }) + }) + + describe('type coercion', () => { + it('should return VALUE error for boolean input', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE(TRUE())'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.NumberCoercion)) + }) + + it('should pass through number input unchanged', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE(123)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toBe(123) + }) + + it('should propagate errors', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE(1/0)'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.DIV_BY_ZERO)) + }) + + it('should convert cell reference with numeric string', () => { + const engine = HyperFormula.buildFromArray([ + ['123', '=VALUE(A1)'], + ]) + + expect(engine.getCellValue(adr('B1'))).toBe(123) + }) + }) + + describe('locale-specific behavior', () => { + it('should respect decimal separator config', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("123,45")'], + ], {decimalSeparator: ',', thousandSeparator: ' '}) + + expect(engine.getCellValue(adr('A1'))).toBe(123.45) + }) + + it('should respect thousand separator config', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("1 234,56")'], + ], {decimalSeparator: ',', thousandSeparator: ' '}) + + expect(engine.getCellValue(adr('A1'))).toBe(1234.56) + }) + + it('should respect custom currency symbol', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("€123")'], + ], {currencySymbol: ['€']}) + + expect(engine.getCellValue(adr('A1'))).toBe(123) + }) + + it('should handle currency symbol at end', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("123€")'], + ], {currencySymbol: ['€']}) + + expect(engine.getCellValue(adr('A1'))).toBe(123) + }) + }) + + describe('custom parseDateTime', () => { + it('should use custom parseDateTime function for date parsing', () => { + const customParseDateTime = jasmine.createSpy().and.callFake((dateString: string) => { + const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateString) + if (match) { + return { + year: parseInt(match[1], 10), + month: parseInt(match[2], 10), + day: parseInt(match[3], 10), + } + } + return undefined + }) + + const engine = HyperFormula.buildFromArray([ + ['=VALUE("2026-01-13")'], + ], { + parseDateTime: customParseDateTime, + dateFormats: ['YYYY-MM-DD'], + }) + + expect(customParseDateTime).toHaveBeenCalled() + expect(engine.getCellValue(adr('A1'))).toBe(46035) + }) + + it('should use custom parseDateTime function for time parsing', () => { + const customParseDateTime = jasmine.createSpy().and.callFake((dateString: string) => { + const match = /^(\d{1,2})h(\d{2})m$/.exec(dateString) + if (match) { + return { + hours: parseInt(match[1], 10), + minutes: parseInt(match[2], 10), + seconds: 0, + } + } + return undefined + }) + + const engine = HyperFormula.buildFromArray([ + ['=VALUE("14h30m")'], + ], { + parseDateTime: customParseDateTime, + timeFormats: ['hh:mm'], + }) + + expect(customParseDateTime).toHaveBeenCalled() + expect(engine.getCellValue(adr('A1'))).toBeCloseTo(0.60416667, 6) + }) + + it('should use custom parseDateTime function for datetime parsing', () => { + const customParseDateTime = jasmine.createSpy().and.callFake((dateString: string) => { + const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/.exec(dateString) + if (match) { + return { + year: parseInt(match[1], 10), + month: parseInt(match[2], 10), + day: parseInt(match[3], 10), + hours: parseInt(match[4], 10), + minutes: parseInt(match[5], 10), + seconds: 0, + } + } + return undefined + }) + + const engine = HyperFormula.buildFromArray([ + ['=VALUE("2026-01-13T14:30")'], + ], { + parseDateTime: customParseDateTime, + dateFormats: ['YYYY-MM-DD'], + timeFormats: ['hh:mm'], + }) + + expect(customParseDateTime).toHaveBeenCalled() + expect(engine.getCellValue(adr('A1'))).toBeCloseTo(46035.60417, 4) + }) + + it('should return VALUE error when custom parseDateTime returns undefined', () => { + const customParseDateTime = jasmine.createSpy().and.returnValue(undefined) + + const engine = HyperFormula.buildFromArray([ + ['=VALUE("invalid-format")'], + ], { + parseDateTime: customParseDateTime, + }) + + expect(customParseDateTime).toHaveBeenCalled() + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.NumberCoercion)) + }) + }) + + describe('return type', () => { + it('should return NUMBER_RAW for numeric string', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("123")'], + ]) + + expect(engine.getCellValueDetailedType(adr('A1'))).toBe(CellValueDetailedType.NUMBER_RAW) + }) + + it('should return NUMBER_DATE for date string', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("01/13/2026")'], + ], {dateFormats: ['MM/DD/YYYY']}) + + expect(engine.getCellValueDetailedType(adr('A1'))).toBe(CellValueDetailedType.NUMBER_DATE) + }) + + it('should return NUMBER_TIME for time string', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("14:30")'], + ]) + + expect(engine.getCellValueDetailedType(adr('A1'))).toBe(CellValueDetailedType.NUMBER_TIME) + }) + + it('should return NUMBER_DATETIME for datetime string', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("01/13/2026 14:30")'], + ], {dateFormats: ['MM/DD/YYYY']}) + + expect(engine.getCellValueDetailedType(adr('A1'))).toBe(CellValueDetailedType.NUMBER_DATETIME) + }) + + it('should return NUMBER_PERCENT for percentage string', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("50%")'], + ]) + + expect(engine.getCellValueDetailedType(adr('A1'))).toBe(CellValueDetailedType.NUMBER_PERCENT) + }) + + it('should return NUMBER_CURRENCY for currency string', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("$123")'], + ]) + + expect(engine.getCellValueDetailedType(adr('A1'))).toBe(CellValueDetailedType.NUMBER_CURRENCY) + }) + }) +}) From 864874e2a32a69398e997f6dfae067885f8bc12f Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Tue, 13 Jan 2026 11:14:33 +0100 Subject: [PATCH 2/7] Function VALUE implementation --- CHANGELOG.md | 1 + docs/guide/built-in-functions.md | 1 + src/i18n/languages/csCZ.ts | 1 + src/i18n/languages/daDK.ts | 1 + src/i18n/languages/deDE.ts | 1 + src/i18n/languages/enGB.ts | 1 + src/i18n/languages/esES.ts | 1 + src/i18n/languages/fiFI.ts | 1 + src/i18n/languages/frFR.ts | 1 + src/i18n/languages/huHU.ts | 1 + src/i18n/languages/itIT.ts | 1 + src/i18n/languages/nbNO.ts | 1 + src/i18n/languages/nlNL.ts | 1 + src/i18n/languages/plPL.ts | 1 + src/i18n/languages/ptPT.ts | 1 + src/i18n/languages/ruRU.ts | 1 + src/i18n/languages/svSE.ts | 1 + src/i18n/languages/trTR.ts | 1 + src/interpreter/ArithmeticHelper.ts | 6 +- src/interpreter/plugin/TextPlugin.ts | 90 +++++++++++++++++++++++++++- 20 files changed, 110 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a3af8b55..a4585b549 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Added a new function: IRR. [#1591](https://github.com/handsontable/hyperformula/issues/1591) - Added a new function: N. [#1585](https://github.com/handsontable/hyperformula/issues/1585) +- Added a new function: VALUE. [#1592](https://github.com/handsontable/hyperformula/issues/1592) ## [3.1.1] - 2025-12-18 diff --git a/docs/guide/built-in-functions.md b/docs/guide/built-in-functions.md index 0847d17ff..f2dfb43d0 100644 --- a/docs/guide/built-in-functions.md +++ b/docs/guide/built-in-functions.md @@ -506,6 +506,7 @@ Total number of functions: **{{ $page.functionsCount }}** | UNICHAR | Returns the character created by using provided code point. | UNICHAR(Number) | | UNICODE | Returns the Unicode code point of a first character of a text. | UNICODE(Text) | | UPPER | Returns text converted to uppercase. | UPPER(Text) | +| VALUE | Converts a text string that represents a number to a number. | VALUE(Text) | [^non-odff]: The return value of this function is compliant with the diff --git a/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts index febe6867b..d0ed619fb 100644 --- a/src/i18n/languages/csCZ.ts +++ b/src/i18n/languages/csCZ.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { UNICHAR: 'UNICHAR', UNICODE: 'UNICODE', UPPER: 'VELKÁ', + VALUE: 'HODNOTA', VARA: 'VARA', 'VAR.P': 'VAR.P', VARPA: 'VARPA', diff --git a/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts index 6ffd42bac..d8838ff19 100644 --- a/src/i18n/languages/daDK.ts +++ b/src/i18n/languages/daDK.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { UNICHAR: 'UNICHAR', UNICODE: 'UNICODE', UPPER: 'STORE.BOGSTAVER', + VALUE: 'VÆRDI', VARA: 'VARIANSV', 'VAR.P': 'VARIANS.P', VARPA: 'VARIANSPV', diff --git a/src/i18n/languages/deDE.ts b/src/i18n/languages/deDE.ts index 55ff125bb..d05f7dcdd 100644 --- a/src/i18n/languages/deDE.ts +++ b/src/i18n/languages/deDE.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { UNICHAR: 'UNIZEICHEN', UNICODE: 'UNICODE', UPPER: 'GROSS', + VALUE: 'WERT', VARA: 'VARIANZA', 'VAR.P': 'VAR.P', VARPA: 'VARIANZENA', diff --git a/src/i18n/languages/enGB.ts b/src/i18n/languages/enGB.ts index 6603d21af..233a354da 100644 --- a/src/i18n/languages/enGB.ts +++ b/src/i18n/languages/enGB.ts @@ -227,6 +227,7 @@ const dictionary: RawTranslationPackage = { UNICHAR: 'UNICHAR', UNICODE: 'UNICODE', UPPER: 'UPPER', + VALUE: 'VALUE', VARA: 'VARA', 'VAR.P': 'VAR.P', VARPA: 'VARPA', diff --git a/src/i18n/languages/esES.ts b/src/i18n/languages/esES.ts index fb40a03ac..7593a79ed 100644 --- a/src/i18n/languages/esES.ts +++ b/src/i18n/languages/esES.ts @@ -225,6 +225,7 @@ export const dictionary: RawTranslationPackage = { UNICHAR: 'UNICHAR', UNICODE: 'UNICODE', UPPER: 'MAYUSC', + VALUE: 'VALOR', VARA: 'VARA', 'VAR.P': 'VAR.P', VARPA: 'VARPA', diff --git a/src/i18n/languages/fiFI.ts b/src/i18n/languages/fiFI.ts index 7b1fdb584..25d54032a 100644 --- a/src/i18n/languages/fiFI.ts +++ b/src/i18n/languages/fiFI.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { UNICHAR: 'UNICODEMERKKI', UNICODE: 'UNICODE', UPPER: 'ISOT', + VALUE: 'ARVO', VARA: 'VARA', 'VAR.P': 'VAR.P', VARPA: 'VARPA', diff --git a/src/i18n/languages/frFR.ts b/src/i18n/languages/frFR.ts index 2f811ad95..7733d20b4 100644 --- a/src/i18n/languages/frFR.ts +++ b/src/i18n/languages/frFR.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { UNICHAR: 'UNICAR', UNICODE: 'UNICODE', UPPER: 'MAJUSCULE', + VALUE: 'CNUM', VARA: 'VARA', 'VAR.P': 'VAR.P.N', VARPA: 'VARPA', diff --git a/src/i18n/languages/huHU.ts b/src/i18n/languages/huHU.ts index e992f97ef..d1341fb21 100644 --- a/src/i18n/languages/huHU.ts +++ b/src/i18n/languages/huHU.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { UNICHAR: 'UNIKARAKTER', UNICODE: 'UNICODE', UPPER: 'NAGYBETŰS', + VALUE: 'ÉRTÉK', VARA: 'VARA', 'VAR.P': 'VAR.S', VARPA: 'VARPA', diff --git a/src/i18n/languages/itIT.ts b/src/i18n/languages/itIT.ts index 6659d5439..28f4ece50 100644 --- a/src/i18n/languages/itIT.ts +++ b/src/i18n/languages/itIT.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { UNICHAR: 'CARATT.UNI', UNICODE: 'UNICODE', UPPER: 'MAIUSC', + VALUE: 'VALORE', VARA: 'VAR.VALORI', 'VAR.P': 'VAR.P', VARPA: 'VAR.POP.VALORI', diff --git a/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts index 729fe1042..61ec76ccf 100644 --- a/src/i18n/languages/nbNO.ts +++ b/src/i18n/languages/nbNO.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { UNICHAR: 'UNICODETEGN', UNICODE: 'UNICODE', UPPER: 'STORE', + VALUE: 'VERDI', VARA: 'VARIANSA', 'VAR.P': 'VARIANS.P', VARPA: 'VARIANSPA', diff --git a/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts index 0407a479f..4de49b7a4 100644 --- a/src/i18n/languages/nlNL.ts +++ b/src/i18n/languages/nlNL.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { UNICHAR: 'UNITEKEN', UNICODE: 'UNICODE', UPPER: 'HOOFDLETTERS', + VALUE: 'WAARDE', VARA: 'VARA', 'VAR.P': 'VAR.P', VARPA: 'VARPA', diff --git a/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts index 3d6a941d5..7c0a08756 100644 --- a/src/i18n/languages/plPL.ts +++ b/src/i18n/languages/plPL.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { UNICHAR: 'ZNAK.UNICODE', UNICODE: 'UNICODE', UPPER: 'LITERY.WIELKIE', + VALUE: 'WARTOŚĆ', VARA: 'WARIANCJA.A', 'VAR.P': 'WARIANCJA.POP', VARPA: 'WARIANCJA.POPUL.A', diff --git a/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts index 1679748c7..cd3fc715d 100644 --- a/src/i18n/languages/ptPT.ts +++ b/src/i18n/languages/ptPT.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { UNICHAR: 'CARACTUNI', UNICODE: 'UNICODE', UPPER: 'MAIÚSCULA', + VALUE: 'VALOR', VARA: 'VARA', 'VAR.P': 'VAR.P', VARPA: 'VARPA', diff --git a/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts index 69a56fd8f..66032d3cd 100644 --- a/src/i18n/languages/ruRU.ts +++ b/src/i18n/languages/ruRU.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { UNICHAR: 'ЮНИСИМВ', UNICODE: 'UNICODE', UPPER: 'ПРОПИСН', + VALUE: 'ЗНАЧ', VARA: 'ДИСПА', 'VAR.P': 'ДИСП.Г', VARPA: 'ДИСПРА', diff --git a/src/i18n/languages/svSE.ts b/src/i18n/languages/svSE.ts index ceaf0649d..d4741d6fc 100644 --- a/src/i18n/languages/svSE.ts +++ b/src/i18n/languages/svSE.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { UNICHAR: 'UNICHAR', UNICODE: 'UNICODE', UPPER: 'VERSALER', + VALUE: 'TEXTNUM', VARA: 'VARA', 'VAR.P': 'VARIANS.P', VARPA: 'VARPA', diff --git a/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts index 3255b0a7d..38507c24b 100644 --- a/src/i18n/languages/trTR.ts +++ b/src/i18n/languages/trTR.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { UNICHAR: 'UNICODEKARAKTERİ', UNICODE: 'UNICODE', UPPER: 'BÜYÜKHARF', + VALUE: 'DEĞER', VARA: 'VARA', 'VAR.P': 'VAR.P', VARPA: 'VARSA', diff --git a/src/interpreter/ArithmeticHelper.ts b/src/interpreter/ArithmeticHelper.ts index 73719a60f..b0a8926a7 100644 --- a/src/interpreter/ArithmeticHelper.ts +++ b/src/interpreter/ArithmeticHelper.ts @@ -47,7 +47,7 @@ export class ArithmeticHelper { constructor( private readonly config: Config, private readonly dateTimeHelper: DateTimeHelper, - private readonly numberLiteralsHelper: NumberLiteralHelper, + public readonly numberLiteralsHelper: NumberLiteralHelper, ) { this.collator = collatorFromConfig(config) this.actualEps = config.smartRounding ? config.precisionEpsilon : 0 @@ -250,7 +250,7 @@ export class ArithmeticHelper { } } - private coerceStringToMaybePercentNumber(input: string): Maybe { + public coerceStringToMaybePercentNumber(input: string): Maybe { const trimmedInput = input.trim() if (trimmedInput.endsWith('%')) { @@ -264,7 +264,7 @@ export class ArithmeticHelper { return undefined } - private coerceStringToMaybeCurrencyNumber(input: string): Maybe { + public coerceStringToMaybeCurrencyNumber(input: string): Maybe { const matchedCurrency = this.currencyMatcher(input.trim()) if (matchedCurrency !== undefined) { diff --git a/src/interpreter/plugin/TextPlugin.ts b/src/interpreter/plugin/TextPlugin.ts index c9802828b..0ea09609f 100644 --- a/src/interpreter/plugin/TextPlugin.ts +++ b/src/interpreter/plugin/TextPlugin.ts @@ -7,8 +7,8 @@ import {CellError, ErrorType} from '../../Cell' import {ErrorMessage} from '../../error-message' import {ProcedureAst} from '../../parser' import {InterpreterState} from '../InterpreterState' -import {InternalScalarValue, InterpreterValue, RawScalarValue} from '../InterpreterValue' import {SimpleRangeValue} from '../../SimpleRangeValue' +import {ExtendedNumber, InterpreterValue, isExtendedNumber, RawScalarValue, InternalScalarValue} from '../InterpreterValue' import {FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions} from './FunctionPlugin' /** @@ -149,6 +149,12 @@ export class TextPlugin extends FunctionPlugin implements FunctionPluginTypechec {argumentType: FunctionArgumentType.STRING} ] }, + 'VALUE': { + method: 'value', + parameters: [ + {argumentType: FunctionArgumentType.SCALAR} + ] + }, } /** @@ -376,6 +382,88 @@ export class TextPlugin extends FunctionPlugin implements FunctionPluginTypechec }) } + /** + * Corresponds to VALUE(text) + * + * Converts a text string that represents a number to a number. + * + * @param ast + * @param state + */ + public value(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('VALUE'), (arg: RawScalarValue): ExtendedNumber | CellError => { + if (arg instanceof CellError) { + return arg + } + + if (isExtendedNumber(arg)) { + return arg + } + + if (typeof arg === 'boolean') { + return new CellError(ErrorType.VALUE, ErrorMessage.NumberCoercion) + } + + if (typeof arg === 'string') { + if (arg === '') { + return new CellError(ErrorType.VALUE, ErrorMessage.NumberCoercion) + } + + const trimmedArg = arg.trim() + + // Try parsing parentheses notation for negative numbers: "(123)" -> -123 + const parenthesesMatch = /^\(([^()]+)\)$/.exec(trimmedArg) + if (parenthesesMatch) { + const innerValue = this.parseStringToNumber(parenthesesMatch[1]) + if (innerValue !== undefined) { + return -innerValue + } + } + + // Try standard parsing + const parsedValue = this.parseStringToNumber(trimmedArg) + if (parsedValue !== undefined) { + return parsedValue + } + + return new CellError(ErrorType.VALUE, ErrorMessage.NumberCoercion) + } + + return new CellError(ErrorType.VALUE, ErrorMessage.NumberCoercion) + }) + } + + /** + * Parses a string to a number, supporting percentages, currencies, numeric strings, and date/time formats. + */ + private parseStringToNumber(input: string): ExtendedNumber | undefined { + // Try percentage + const percentResult = this.arithmeticHelper.coerceStringToMaybePercentNumber(input) + if (percentResult !== undefined) { + return percentResult + } + + // Try currency + const currencyResult = this.arithmeticHelper.coerceStringToMaybeCurrencyNumber(input) + if (currencyResult !== undefined) { + return currencyResult + } + + // Try plain number + const numberResult = this.arithmeticHelper.numberLiteralsHelper.numericStringToMaybeNumber(input.trim()) + if (numberResult !== undefined) { + return numberResult + } + + // Try date/time + const dateTimeResult = this.dateTimeHelper.dateStringToDateNumber(input) + if (dateTimeResult !== undefined) { + return dateTimeResult + } + + return undefined + } + private escapeRegExpSpecialCharacters(text: string): string { return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } From 4dbda00ff13ebd81b28a1f8417e40904236a069e Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Tue, 13 Jan 2026 11:32:21 +0100 Subject: [PATCH 3/7] Refactor --- src/interpreter/ArithmeticHelper.ts | 41 +++++++++++++++++-- src/interpreter/plugin/TextPlugin.ts | 35 +--------------- test/unit/interpreter/function-value.spec.ts | 43 ++++++++++++++++---- 3 files changed, 75 insertions(+), 44 deletions(-) diff --git a/src/interpreter/ArithmeticHelper.ts b/src/interpreter/ArithmeticHelper.ts index b0a8926a7..33c1f6c0b 100644 --- a/src/interpreter/ArithmeticHelper.ts +++ b/src/interpreter/ArithmeticHelper.ts @@ -47,7 +47,7 @@ export class ArithmeticHelper { constructor( private readonly config: Config, private readonly dateTimeHelper: DateTimeHelper, - public readonly numberLiteralsHelper: NumberLiteralHelper, + private readonly numberLiteralsHelper: NumberLiteralHelper, ) { this.collator = collatorFromConfig(config) this.actualEps = config.smartRounding ? config.precisionEpsilon : 0 @@ -222,6 +222,41 @@ export class ArithmeticHelper { ) } + /** + * Parses a string to a number, supporting percentages, currencies, numeric strings, and date/time formats. + * Unlike coerceNonDateScalarToMaybeNumber, this also handles scientific notation with uppercase E. + */ + public parseStringToNumber(input: string): Maybe { + const trimmedInput = input.trim() + + // Try percentage + const percentResult = this.coerceStringToMaybePercentNumber(trimmedInput) + if (percentResult !== undefined) { + return percentResult + } + + // Try currency + const currencyResult = this.coerceStringToMaybeCurrencyNumber(trimmedInput) + if (currencyResult !== undefined) { + return currencyResult + } + + // Try plain number (normalize scientific notation E to e) + const normalizedInput = trimmedInput.replace(/E/g, 'e') + const numberResult = this.numberLiteralsHelper.numericStringToMaybeNumber(normalizedInput) + if (numberResult !== undefined) { + return numberResult + } + + // Try date/time + const dateTimeResult = this.dateTimeHelper.dateStringToDateNumber(trimmedInput) + if (dateTimeResult !== undefined) { + return dateTimeResult + } + + return undefined + } + public coerceNonDateScalarToMaybeNumber(arg: InternalScalarValue): Maybe { if (arg === EmptyValue) { return 0 @@ -250,7 +285,7 @@ export class ArithmeticHelper { } } - public coerceStringToMaybePercentNumber(input: string): Maybe { + private coerceStringToMaybePercentNumber(input: string): Maybe { const trimmedInput = input.trim() if (trimmedInput.endsWith('%')) { @@ -264,7 +299,7 @@ export class ArithmeticHelper { return undefined } - public coerceStringToMaybeCurrencyNumber(input: string): Maybe { + private coerceStringToMaybeCurrencyNumber(input: string): Maybe { const matchedCurrency = this.currencyMatcher(input.trim()) if (matchedCurrency !== undefined) { diff --git a/src/interpreter/plugin/TextPlugin.ts b/src/interpreter/plugin/TextPlugin.ts index 0ea09609f..664d306bb 100644 --- a/src/interpreter/plugin/TextPlugin.ts +++ b/src/interpreter/plugin/TextPlugin.ts @@ -414,14 +414,14 @@ export class TextPlugin extends FunctionPlugin implements FunctionPluginTypechec // Try parsing parentheses notation for negative numbers: "(123)" -> -123 const parenthesesMatch = /^\(([^()]+)\)$/.exec(trimmedArg) if (parenthesesMatch) { - const innerValue = this.parseStringToNumber(parenthesesMatch[1]) + const innerValue = this.arithmeticHelper.parseStringToNumber(parenthesesMatch[1]) if (innerValue !== undefined) { return -innerValue } } // Try standard parsing - const parsedValue = this.parseStringToNumber(trimmedArg) + const parsedValue = this.arithmeticHelper.parseStringToNumber(trimmedArg) if (parsedValue !== undefined) { return parsedValue } @@ -433,37 +433,6 @@ export class TextPlugin extends FunctionPlugin implements FunctionPluginTypechec }) } - /** - * Parses a string to a number, supporting percentages, currencies, numeric strings, and date/time formats. - */ - private parseStringToNumber(input: string): ExtendedNumber | undefined { - // Try percentage - const percentResult = this.arithmeticHelper.coerceStringToMaybePercentNumber(input) - if (percentResult !== undefined) { - return percentResult - } - - // Try currency - const currencyResult = this.arithmeticHelper.coerceStringToMaybeCurrencyNumber(input) - if (currencyResult !== undefined) { - return currencyResult - } - - // Try plain number - const numberResult = this.arithmeticHelper.numberLiteralsHelper.numericStringToMaybeNumber(input.trim()) - if (numberResult !== undefined) { - return numberResult - } - - // Try date/time - const dateTimeResult = this.dateTimeHelper.dateStringToDateNumber(input) - if (dateTimeResult !== undefined) { - return dateTimeResult - } - - return undefined - } - private escapeRegExpSpecialCharacters(text: string): string { return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } diff --git a/test/unit/interpreter/function-value.spec.ts b/test/unit/interpreter/function-value.spec.ts index 05685444e..c03ab9e55 100644 --- a/test/unit/interpreter/function-value.spec.ts +++ b/test/unit/interpreter/function-value.spec.ts @@ -83,7 +83,7 @@ describe('Function VALUE', () => { it('should convert string with thousand separator', () => { const engine = HyperFormula.buildFromArray([ ['=VALUE("1,234")'], - ]) + ], {thousandSeparator: ',', functionArgSeparator: ';'}) expect(engine.getCellValue(adr('A1'))).toBe(1234) }) @@ -95,6 +95,22 @@ describe('Function VALUE', () => { expect(engine.getCellValue(adr('A1'))).toBe(-123) }) + + it('should return VALUE error for nested parentheses', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("((123))")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.NumberCoercion)) + }) + + it('should return VALUE error for unbalanced parentheses', () => { + const engine = HyperFormula.buildFromArray([ + ['=VALUE("(123")'], + ]) + + expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.NumberCoercion)) + }) }) describe('percentage strings', () => { @@ -119,7 +135,7 @@ describe('Function VALUE', () => { it('should convert currency string with thousand separator and decimal', () => { const engine = HyperFormula.buildFromArray([ ['=VALUE("$1,234.56")'], - ]) + ], {thousandSeparator: ',', functionArgSeparator: ';'}) expect(engine.getCellValue(adr('A1'))).toBe(1234.56) }) @@ -204,12 +220,15 @@ describe('Function VALUE', () => { expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.NumberCoercion)) }) - it('should return VALUE error for 12-hour time format without proper config', () => { + }) + + describe('12-hour time format', () => { + it('should parse 12-hour time format with am/pm', () => { const engine = HyperFormula.buildFromArray([ ['=VALUE("3:00pm")'], ]) - expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.NumberCoercion)) + expect(engine.getCellValue(adr('A1'))).toBeCloseTo(0.625, 6) }) }) @@ -238,9 +257,17 @@ describe('Function VALUE', () => { expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.DIV_BY_ZERO)) }) - it('should convert cell reference with numeric string', () => { + it('should convert cell reference with string value', () => { + const engine = HyperFormula.buildFromArray([ + ["'123", '=VALUE(A1)'], + ]) + + expect(engine.getCellValue(adr('B1'))).toBe(123) + }) + + it('should pass through cell reference with numeric value', () => { const engine = HyperFormula.buildFromArray([ - ['123', '=VALUE(A1)'], + [123, '=VALUE(A1)'], ]) expect(engine.getCellValue(adr('B1'))).toBe(123) @@ -251,7 +278,7 @@ describe('Function VALUE', () => { it('should respect decimal separator config', () => { const engine = HyperFormula.buildFromArray([ ['=VALUE("123,45")'], - ], {decimalSeparator: ',', thousandSeparator: ' '}) + ], {decimalSeparator: ',', thousandSeparator: ' ', functionArgSeparator: ';'}) expect(engine.getCellValue(adr('A1'))).toBe(123.45) }) @@ -259,7 +286,7 @@ describe('Function VALUE', () => { it('should respect thousand separator config', () => { const engine = HyperFormula.buildFromArray([ ['=VALUE("1 234,56")'], - ], {decimalSeparator: ',', thousandSeparator: ' '}) + ], {decimalSeparator: ',', thousandSeparator: ' ', functionArgSeparator: ';'}) expect(engine.getCellValue(adr('A1'))).toBe(1234.56) }) From dc8f482bf687595612e2dc1ced9e6133806fcb3e Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Tue, 13 Jan 2026 12:07:49 +0100 Subject: [PATCH 4/7] Reduce codeduplication --- docs/guide/built-in-functions.md | 2 +- package.json | 2 +- src/CellContentParser.ts | 4 +-- src/NumberLiteralHelper.ts | 2 +- src/interpreter/ArithmeticHelper.ts | 35 ---------------------- src/interpreter/plugin/TextPlugin.ts | 43 ++++++++++++++-------------- 6 files changed, 27 insertions(+), 61 deletions(-) diff --git a/docs/guide/built-in-functions.md b/docs/guide/built-in-functions.md index f2dfb43d0..dd9777523 100644 --- a/docs/guide/built-in-functions.md +++ b/docs/guide/built-in-functions.md @@ -506,7 +506,7 @@ Total number of functions: **{{ $page.functionsCount }}** | UNICHAR | Returns the character created by using provided code point. | UNICHAR(Number) | | UNICODE | Returns the Unicode code point of a first character of a text. | UNICODE(Text) | | UPPER | Returns text converted to uppercase. | UPPER(Text) | -| VALUE | Converts a text string that represents a number to a number. | VALUE(Text) | +| VALUE | Parses a number, date, time, datetime, currency, or percentage from a text string. | VALUE(Text) | [^non-odff]: The return value of this function is compliant with the diff --git a/package.json b/package.json index ca27ce7fc..230f7ab6c 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "test": "npm-run-all lint test:unit test:browser test:compatibility", "test:unit": "cross-env NODE_ICU_DATA=node_modules/full-icu jest", "test:watch": "cross-env NODE_ICU_DATA=node_modules/full-icu jest --watch", - "test:tdd": "cross-env NODE_ICU_DATA=node_modules/full-icu jest --watch adding-sheet", + "test:tdd": "cross-env NODE_ICU_DATA=node_modules/full-icu jest --watch function-value", "test:coverage": "npm run test:unit -- --coverage", "test:logMemory": "cross-env NODE_ICU_DATA=node_modules/full-icu jest --runInBand --logHeapUsage", "test:unit.ci": "cross-env NODE_ICU_DATA=node_modules/full-icu node --expose-gc ./node_modules/jest/bin/jest --forceExit", diff --git a/src/CellContentParser.ts b/src/CellContentParser.ts index 916068ae0..457e4dfb8 100644 --- a/src/CellContentParser.ts +++ b/src/CellContentParser.ts @@ -139,8 +139,8 @@ export class CellContentParser { } else { let trimmedContent = content.trim() let mode = 0 - let currency - if (trimmedContent.endsWith('%')) { + let currency // currency + if (trimmedContent.endsWith('%')) { // percentage mode = 1 trimmedContent = trimmedContent.slice(0, trimmedContent.length - 1) } else { diff --git a/src/NumberLiteralHelper.ts b/src/NumberLiteralHelper.ts index 84aff54b7..8d7a526ce 100644 --- a/src/NumberLiteralHelper.ts +++ b/src/NumberLiteralHelper.ts @@ -16,7 +16,7 @@ export class NumberLiteralHelper { const thousandSeparator = this.config.thousandSeparator === '.' ? `\\${this.config.thousandSeparator}` : this.config.thousandSeparator const decimalSeparator = this.config.decimalSeparator === '.' ? `\\${this.config.decimalSeparator}` : this.config.decimalSeparator - this.numberPattern = new RegExp(`^([+-]?((${decimalSeparator}\\d+)|(\\d+(${thousandSeparator}\\d{3,})*(${decimalSeparator}\\d*)?)))(e[+-]?\\d+)?$`) + this.numberPattern = new RegExp(`^([+-]?((${decimalSeparator}\\d+)|(\\d+(${thousandSeparator}\\d{3,})*(${decimalSeparator}\\d*)?)))([eE][+-]?\\d+)?$`) this.allThousandSeparatorsRegex = new RegExp(`${thousandSeparator}`, 'g') } diff --git a/src/interpreter/ArithmeticHelper.ts b/src/interpreter/ArithmeticHelper.ts index 33c1f6c0b..73719a60f 100644 --- a/src/interpreter/ArithmeticHelper.ts +++ b/src/interpreter/ArithmeticHelper.ts @@ -222,41 +222,6 @@ export class ArithmeticHelper { ) } - /** - * Parses a string to a number, supporting percentages, currencies, numeric strings, and date/time formats. - * Unlike coerceNonDateScalarToMaybeNumber, this also handles scientific notation with uppercase E. - */ - public parseStringToNumber(input: string): Maybe { - const trimmedInput = input.trim() - - // Try percentage - const percentResult = this.coerceStringToMaybePercentNumber(trimmedInput) - if (percentResult !== undefined) { - return percentResult - } - - // Try currency - const currencyResult = this.coerceStringToMaybeCurrencyNumber(trimmedInput) - if (currencyResult !== undefined) { - return currencyResult - } - - // Try plain number (normalize scientific notation E to e) - const normalizedInput = trimmedInput.replace(/E/g, 'e') - const numberResult = this.numberLiteralsHelper.numericStringToMaybeNumber(normalizedInput) - if (numberResult !== undefined) { - return numberResult - } - - // Try date/time - const dateTimeResult = this.dateTimeHelper.dateStringToDateNumber(trimmedInput) - if (dateTimeResult !== undefined) { - return dateTimeResult - } - - return undefined - } - public coerceNonDateScalarToMaybeNumber(arg: InternalScalarValue): Maybe { if (arg === EmptyValue) { return 0 diff --git a/src/interpreter/plugin/TextPlugin.ts b/src/interpreter/plugin/TextPlugin.ts index 664d306bb..886306d68 100644 --- a/src/interpreter/plugin/TextPlugin.ts +++ b/src/interpreter/plugin/TextPlugin.ts @@ -5,6 +5,7 @@ import {CellError, ErrorType} from '../../Cell' import {ErrorMessage} from '../../error-message' +import { Maybe } from '../../Maybe' import {ProcedureAst} from '../../parser' import {InterpreterState} from '../InterpreterState' import {SimpleRangeValue} from '../../SimpleRangeValue' @@ -400,39 +401,39 @@ export class TextPlugin extends FunctionPlugin implements FunctionPluginTypechec return arg } - if (typeof arg === 'boolean') { + if (typeof arg !== 'string') { return new CellError(ErrorType.VALUE, ErrorMessage.NumberCoercion) } - if (typeof arg === 'string') { - if (arg === '') { - return new CellError(ErrorType.VALUE, ErrorMessage.NumberCoercion) - } - - const trimmedArg = arg.trim() + const trimmedArg = arg.trim() - // Try parsing parentheses notation for negative numbers: "(123)" -> -123 - const parenthesesMatch = /^\(([^()]+)\)$/.exec(trimmedArg) - if (parenthesesMatch) { - const innerValue = this.arithmeticHelper.parseStringToNumber(parenthesesMatch[1]) - if (innerValue !== undefined) { - return -innerValue - } - } - - // Try standard parsing - const parsedValue = this.arithmeticHelper.parseStringToNumber(trimmedArg) - if (parsedValue !== undefined) { - return parsedValue + const parenthesesMatch = /^\(([^()]+)\)$/.exec(trimmedArg) + if (parenthesesMatch) { + const innerValue = this.parseStringToNumber(parenthesesMatch[1]) + if (innerValue !== undefined) { + return -innerValue } + } - return new CellError(ErrorType.VALUE, ErrorMessage.NumberCoercion) + const parsedValue = this.parseStringToNumber(trimmedArg) + if (parsedValue !== undefined) { + return parsedValue } return new CellError(ErrorType.VALUE, ErrorMessage.NumberCoercion) }) } + private parseStringToNumber(input: string): Maybe { + const trimmedInput = input.trim() + + if (trimmedInput === '') { + return undefined + } + + return this.arithmeticHelper.coerceToMaybeNumber(trimmedInput) + } + private escapeRegExpSpecialCharacters(text: string): string { return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } From fecc59d5c939308962e6f534a1ae7eadbff80656 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Tue, 13 Jan 2026 12:18:42 +0100 Subject: [PATCH 5/7] Remove redundant comments --- src/CellContentParser.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CellContentParser.ts b/src/CellContentParser.ts index 457e4dfb8..916068ae0 100644 --- a/src/CellContentParser.ts +++ b/src/CellContentParser.ts @@ -139,8 +139,8 @@ export class CellContentParser { } else { let trimmedContent = content.trim() let mode = 0 - let currency // currency - if (trimmedContent.endsWith('%')) { // percentage + let currency + if (trimmedContent.endsWith('%')) { mode = 1 trimmedContent = trimmedContent.slice(0, trimmedContent.length - 1) } else { From cc5043620a18db69c1d79cb48bc5fe46137c724c Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Tue, 13 Jan 2026 12:48:25 +0100 Subject: [PATCH 6/7] Fix some linter errors --- src/interpreter/plugin/TextPlugin.ts | 18 ++++++++++++------ test/unit/interpreter/function-value.spec.ts | 8 ++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/interpreter/plugin/TextPlugin.ts b/src/interpreter/plugin/TextPlugin.ts index 886306d68..1786af334 100644 --- a/src/interpreter/plugin/TextPlugin.ts +++ b/src/interpreter/plugin/TextPlugin.ts @@ -163,8 +163,8 @@ export class TextPlugin extends FunctionPlugin implements FunctionPluginTypechec * * Concatenates provided arguments to one string. * - * @param ast - * @param state + * @param {ProcedureAst} ast - The procedure AST node + * @param {InterpreterState} state - The interpreter state */ public concatenate(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.runFunction(ast.args, state, this.metadata('CONCATENATE'), (...args) => { @@ -177,8 +177,8 @@ export class TextPlugin extends FunctionPlugin implements FunctionPluginTypechec * * Splits provided string using space separator and returns chunk at zero-based position specified by second argument * - * @param ast - * @param state + * @param {ProcedureAst} ast - The procedure AST node + * @param {InterpreterState} state - The interpreter state */ public split(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.runFunction(ast.args, state, this.metadata('SPLIT'), (stringToSplit: string, indexToUse: number) => { @@ -388,8 +388,8 @@ export class TextPlugin extends FunctionPlugin implements FunctionPluginTypechec * * Converts a text string that represents a number to a number. * - * @param ast - * @param state + * @param {ProcedureAst} ast - The procedure AST node + * @param {InterpreterState} state - The interpreter state */ public value(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.runFunction(ast.args, state, this.metadata('VALUE'), (arg: RawScalarValue): ExtendedNumber | CellError => { @@ -424,6 +424,12 @@ export class TextPlugin extends FunctionPlugin implements FunctionPluginTypechec }) } + /** + * Parses a string to a numeric value, handling whitespace trimming and empty string validation. + * + * @param {string} input - The string to parse + * @returns {Maybe} The parsed number or undefined if parsing fails or input is empty + */ private parseStringToNumber(input: string): Maybe { const trimmedInput = input.trim() diff --git a/test/unit/interpreter/function-value.spec.ts b/test/unit/interpreter/function-value.spec.ts index c03ab9e55..0687f3c70 100644 --- a/test/unit/interpreter/function-value.spec.ts +++ b/test/unit/interpreter/function-value.spec.ts @@ -329,7 +329,7 @@ describe('Function VALUE', () => { dateFormats: ['YYYY-MM-DD'], }) - expect(customParseDateTime).toHaveBeenCalled() + expect(customParseDateTime).toHaveBeenCalledWith('2026-01-13', 'YYYY-MM-DD', 'hh:mm') expect(engine.getCellValue(adr('A1'))).toBe(46035) }) @@ -353,7 +353,7 @@ describe('Function VALUE', () => { timeFormats: ['hh:mm'], }) - expect(customParseDateTime).toHaveBeenCalled() + expect(customParseDateTime).toHaveBeenCalledWith('14h30m', 'DD/MM/YYYY', 'hh:mm') expect(engine.getCellValue(adr('A1'))).toBeCloseTo(0.60416667, 6) }) @@ -381,7 +381,7 @@ describe('Function VALUE', () => { timeFormats: ['hh:mm'], }) - expect(customParseDateTime).toHaveBeenCalled() + expect(customParseDateTime).toHaveBeenCalledWith('2026-01-13T14:30', 'YYYY-MM-DD', 'hh:mm') expect(engine.getCellValue(adr('A1'))).toBeCloseTo(46035.60417, 4) }) @@ -394,7 +394,7 @@ describe('Function VALUE', () => { parseDateTime: customParseDateTime, }) - expect(customParseDateTime).toHaveBeenCalled() + expect(customParseDateTime).toHaveBeenCalledWith('invalid-format', 'DD/MM/YYYY', 'hh:mm') expect(engine.getCellValue(adr('A1'))).toEqualError(detailedError(ErrorType.VALUE, ErrorMessage.NumberCoercion)) }) }) From ce3c2787ecfa87abf0ab3063d8233b0fb12dd2c1 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Wed, 14 Jan 2026 10:28:51 +0100 Subject: [PATCH 7/7] Update src/interpreter/plugin/TextPlugin.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Krzysztof ‘Budzio’ Budnik <571316+budnix@users.noreply.github.com> --- src/interpreter/plugin/TextPlugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interpreter/plugin/TextPlugin.ts b/src/interpreter/plugin/TextPlugin.ts index 1786af334..4bb5832df 100644 --- a/src/interpreter/plugin/TextPlugin.ts +++ b/src/interpreter/plugin/TextPlugin.ts @@ -5,7 +5,7 @@ import {CellError, ErrorType} from '../../Cell' import {ErrorMessage} from '../../error-message' -import { Maybe } from '../../Maybe' +import {Maybe} from '../../Maybe' import {ProcedureAst} from '../../parser' import {InterpreterState} from '../InterpreterState' import {SimpleRangeValue} from '../../SimpleRangeValue'