diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ae907f1ab..52a15d789c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed TEXT function rounding issue that caused incorrect conversion of date and time values to strings. [#1043](https://github.com/handsontable/hyperformula/issues/1043) - Fixed functions SUMIF, SUMIFS, AVERAGEIF, COUNTIF, COUNTIFS to handle complex numeric values correctly. [#951](https://github.com/handsontable/hyperformula/issues/951) +- Fixed the rounding strategy in the default time-parsing function to be independent of the `timeFormats` configuration parameter. Time values will be always rounded to the nearest milisecond (0.001 s). [#953](https://github.com/handsontable/hyperformula/issues/953) ## [2.0.1] - 2022-06-14 diff --git a/docs/guide/date-and-time-handling.md b/docs/guide/date-and-time-handling.md index 65ba63cbaa..963f8fcb9a 100644 --- a/docs/guide/date-and-time-handling.md +++ b/docs/guide/date-and-time-handling.md @@ -1,17 +1,10 @@ # Date and time handling -Date and time formats can be set in the -[configuration options](configuration-options.md). +The formats for the default date and time parsing functions can be set using configuration options: +- [dateFormats](../api/interfaces/configparams.md#dateformats), +- [timeFormats](../api/interfaces/configparams.md#timeformats), +- [nullYear](../api/interfaces/configparams.md#nullyear). -`dateFormats` is a list of formats supported by the parser -inside HyperFormula. The default format is -`['MM/DD/YYYY', 'MM/DD/YY']`. The separator is ignored and it can -be any of the following: '-' (dash), ' ' (empty space), -'/' (slash). - -Similar to `dateFormats`, `timeFormats` is a list of time formats -supported by the parser. The default format is -`['hh:mm', 'hh:mm:ss.sss']`. The only accepted separator is ':' (colon). ## Example with Chinese @@ -24,40 +17,31 @@ const options = { }; ``` -## Integration and customization +## Custom functions for handling date and time HyperFormula offers the possibility to extend the number of supported date/time formats as well as the behavior of this functionality by exposing three options: -* `parseDateTime` which allows for providing a function that accepts +- [parseDateTime](../api/interfaces/configparams.md#parsedatetime), which allows to provide a function that accepts a string representing date/time and parses it into an actual date/time format -* `stringifyDateTime`which allows for providing a function that +- [stringifyDateTime](../api/interfaces/configparams.md#stringifydatetime), which allows to provide a function that takes the date/time and prints it as a string -* `stringifyDuration` which allows for providing a function that +- [stringifyDuration](../api/interfaces/configparams.md#stringifyduration), which allows to provide a function that takes time duration and prints it as a string -To extend the number of possible date formats you will need to -configure `parseDateTime` . This functionality is based on callbacks +To extend the number of possible date formats, you will need to +configure `parseDateTime` . This functionality is based on callbacks, and you can customize the formats by integrating a third-party library like [Moment.js](https://momentjs.com/), or by writing your -own custom function like this: - -```javascript - { - year: number, - month: number, - day: number, - } -``` +own custom function that returns a [DateTime](../api/globals.md#datetime) object. -The configuration of date formats and stringify options may have -an impact on how the functions TEXT and VALUE, as well as criterions -(e.g. SUMIF, AVERAGEIF), will behave. For instance, VALUE accepts a string -and returns a number, which means it uses `parseDatetime`. TEXT -works the other way round - it accepts a number and returns a string +The configuration of date formats and stringify options may impact some built-in functions. +For instance, VALUE transforms strings +into numbers, which means it uses `parseDatetime`. TEXT +works the other way round - it accepts a number and returns a string, so it uses `stringifyDateTime`. Any change here might give you -different results. Criterions do comparisons so they also need to +different results. Criteria-based functions (SUMIF, AVERAGEIF, etc.) perform comparisons, so they also need to work on strings, dates, etc. ## Moment.js integration @@ -106,18 +90,11 @@ your custom format: const data = [["31st Jan 00", "2nd Jun 01", "=B1-A1"]]; ``` -And now the operations on dates are possible since HyperFormula -recognizes them as proper dates: +And now, HyperFormula recognizes these values as valid dates and can operate on them. ## Demo - +

@@ -126,116 +103,116 @@ recognizes them as proper dates: Below is a cheat sheet with the most popular date formats in different countries. -| Country | Language | Format | -| :--- | :--- | :--- | -| Albania | Albanian | yyyy-MM-dd | -| United Arab Emirates | Arabic | dd/MM/yyyy | -| Argentina | Spanish | dd/MM/yyyy | -| Australia | English | d/MM/yyyy | -| Austria | German | dd.MM.yyyy | -| Belgium | French | d/MM/yyyy | -| Belgium | Dutch | d/MM/yyyy | -| Bulgaria | Bulgarian | yyyy-M-d | -| Bahrain | Arabic | dd/MM/yyyy | -| Bosnia and Herzegovina | Bosnian | dd.MM.yyyy. | -| Bosnia and Herzegovina | Serbian | dd.MM.yyyy. | -| Bosnia and Herzegovina | Croatian | dd.MM.yyyy. | -| Belarus | Belarusian | d.M.yyyy | -| Bolivia | Spanish | dd-MM-yyyy | -| Brazil | Portuguese | dd/MM/yyyy | -| Canada | French | yyyy-MM-dd | -| Canada | English | dd/MM/yyyy | -| Switzerland | German | dd.MM.yyyy | -| Switzerland | French | dd.MM.yyyy | -| Switzerland | Italian | dd.MM.yyyy | -| Chile | Spanish | dd-MM-yyyy | -| China | Chinese | yyyy-M-d | -| Colombia | Spanish | d/MM/yyyy | -| Costa Rica | Spanish | dd/MM/yyyy | -| Cyprus | Greek | dd/MM/yyyy | -| Czech Republic | Czech | d.M.yyyy | -| Germany | German | dd.MM.yyyy | -| Denmark | Danish | dd-MM-yyyy | -| Dominican Republic | Spanish | MM/dd/yyyy | -| Algeria | Arabic | dd/MM/yyyy | -| Ecuador | Spanish | dd/MM/yyyy | -| Egypt | Arabic | dd/MM/yyyy | -| Spain | Spanish | d/MM/yyyy | -| Spain | Catalan | dd/MM/yyyy | -| Estonia | Estonian | d.MM.yyyy | -| Finland | Finnish | d.M.yyyy | -| France | French | dd/MM/yyyy | -| United Kingdom | English | dd/MM/yyyy | -| Greece | Greek | d/M/yyyy | -| Guatemala | Spanish | d/MM/yyyy | -| Hong Kong | Chinese | yyyy年M月d日 | -| Honduras | Spanish | MM-dd-yyyy | -| Croatia | Croatian | dd.MM.yyyy. | -| Hungary | Hungarian | yyyy.MM.dd. | -| Indonesia | Indonesian | dd/MM/yyyy | -| India | Hindi | ३/६/१२ | -| India | English | d/M/yyyy | -| Ireland | Irish | dd/MM/yyyy | -| Ireland | English | dd/MM/yyyy | -| Iraq | Arabic | dd/MM/yyyy | -| Iceland | Icelandic | d.M.yyyy | -| Israel | Hebrew | dd/MM/yyyy | -| Italy | Italian | dd/MM/yyyy | -| Jordan | Arabic | dd/MM/yyyy | -| Japan | Japanese | yyyy/MM/dd | -| Japan | Japanese | H24.MM.dd | -| South Korea | Korean | yyyy. M. d | -| Kuwait | Arabic | dd/MM/yyyy | -| Lebanon | Arabic | dd/MM/yyyy | -| Libya | Arabic | dd/MM/yyyy | -| Lithuania | Lithuanian | yyyy.M.d | -| Luxembourg | French | dd/MM/yyyy | -| Luxembourg | German | dd.MM.yyyy | -| Latvia | Latvian | yyyy.d.M | -| Morocco | Arabic | dd/MM/yyyy | -| Mexico | Spanish | d/MM/yyyy | -| Macedonia | Macedonian | d.M.yyyy | -| Malta | English | dd/MM/yyyy | -| Malta | Maltese | dd/MM/yyyy | -| Montenegro | Serbian | d.M.yyyy. | -| Malaysia | Malay | dd/MM/yyyy | -| Nicaragua | Spanish | MM-dd-yyyy | -| Netherlands | Dutch | d-M-yyyy | -| Norway | Norwegian | dd.MM.yyyy | -| Norway | Norwegian | dd.MM.yyyy | -| New Zealand | English | d/MM/yyyy | -| Oman | Arabic | dd/MM/yyyy | -| Panama | Spanish | MM/dd/yyyy | -| Peru | Spanish | dd/MM/yyyy | -| Philippines | English | M/d/yyyy | -| Poland | Polish | dd.MM.yyyy | -| Puerto Rico | Spanish | MM-dd-yyyy | -| Portugal | Portuguese | dd-MM-yyyy | -| Paraguay | Spanish | dd/MM/yyyy | -| Qatar | Arabic | dd/MM/yyyy | -| Romania | Romanian | dd.MM.yyyy | -| Russia | Russian | dd.MM.yyyy | -| Saudi Arabia | Arabic | dd/MM/yyyy | -| Serbia and Montenegro | Serbian | d.M.yyyy. | -| Sudan | Arabic | dd/MM/yyyy | -| Singapore | Chinese | dd/MM/yyyy | -| Singapore | English | M/d/yyyy | -| El Salvador | Spanish | MM-dd-yyyy | -| Serbia | Serbian | d.M.yyyy. | -| Slovakia | Slovak | d.M.yyyy | -| Slovenia | Slovenian | d.M.yyyy | -| Sweden | Swedish | yyyy-MM-dd | -| Syria | Arabic | dd/MM/yyyy | -| Thailand | Thai | d/M/2555 | -| Thailand | Thai | ๓/๖/๒๕๕๕ | -| Tunisia | Arabic | dd/MM/yyyy | -| Turkey | Turkish | dd.MM.yyyy | -| Taiwan | Chinese | yyyy/M/d | -| Ukraine | Ukrainian | dd.MM.yyyy | -| Uruguay | Spanish | dd/MM/yyyy | -| United States | English | M/d/yyyy | -| United States | Spanish | M/d/yyyy | -| Venezuela | Spanish | dd/MM/yyyy | -| Vietnam | Vietnamese | dd/MM/yyyy | -| Yemen | Arabic | dd/MM/yyyy | -| South Africa | English | yyyy/MM/dd | +| Country | Language | Format | +|:-----------------------|:-----------|:------------| +| Albania | Albanian | yyyy-MM-dd | +| United Arab Emirates | Arabic | dd/MM/yyyy | +| Argentina | Spanish | dd/MM/yyyy | +| Australia | English | d/MM/yyyy | +| Austria | German | dd.MM.yyyy | +| Belgium | French | d/MM/yyyy | +| Belgium | Dutch | d/MM/yyyy | +| Bulgaria | Bulgarian | yyyy-M-d | +| Bahrain | Arabic | dd/MM/yyyy | +| Bosnia and Herzegovina | Bosnian | dd.MM.yyyy. | +| Bosnia and Herzegovina | Serbian | dd.MM.yyyy. | +| Bosnia and Herzegovina | Croatian | dd.MM.yyyy. | +| Belarus | Belarusian | d.M.yyyy | +| Bolivia | Spanish | dd-MM-yyyy | +| Brazil | Portuguese | dd/MM/yyyy | +| Canada | French | yyyy-MM-dd | +| Canada | English | dd/MM/yyyy | +| Switzerland | German | dd.MM.yyyy | +| Switzerland | French | dd.MM.yyyy | +| Switzerland | Italian | dd.MM.yyyy | +| Chile | Spanish | dd-MM-yyyy | +| China | Chinese | yyyy-M-d | +| Colombia | Spanish | d/MM/yyyy | +| Costa Rica | Spanish | dd/MM/yyyy | +| Cyprus | Greek | dd/MM/yyyy | +| Czech Republic | Czech | d.M.yyyy | +| Germany | German | dd.MM.yyyy | +| Denmark | Danish | dd-MM-yyyy | +| Dominican Republic | Spanish | MM/dd/yyyy | +| Algeria | Arabic | dd/MM/yyyy | +| Ecuador | Spanish | dd/MM/yyyy | +| Egypt | Arabic | dd/MM/yyyy | +| Spain | Spanish | d/MM/yyyy | +| Spain | Catalan | dd/MM/yyyy | +| Estonia | Estonian | d.MM.yyyy | +| Finland | Finnish | d.M.yyyy | +| France | French | dd/MM/yyyy | +| United Kingdom | English | dd/MM/yyyy | +| Greece | Greek | d/M/yyyy | +| Guatemala | Spanish | d/MM/yyyy | +| Hong Kong | Chinese | yyyy年M月d日 | +| Honduras | Spanish | MM-dd-yyyy | +| Croatia | Croatian | dd.MM.yyyy. | +| Hungary | Hungarian | yyyy.MM.dd. | +| Indonesia | Indonesian | dd/MM/yyyy | +| India | Hindi | ३/६/१२ | +| India | English | d/M/yyyy | +| Ireland | Irish | dd/MM/yyyy | +| Ireland | English | dd/MM/yyyy | +| Iraq | Arabic | dd/MM/yyyy | +| Iceland | Icelandic | d.M.yyyy | +| Israel | Hebrew | dd/MM/yyyy | +| Italy | Italian | dd/MM/yyyy | +| Jordan | Arabic | dd/MM/yyyy | +| Japan | Japanese | yyyy/MM/dd | +| Japan | Japanese | H24.MM.dd | +| South Korea | Korean | yyyy. M. d | +| Kuwait | Arabic | dd/MM/yyyy | +| Lebanon | Arabic | dd/MM/yyyy | +| Libya | Arabic | dd/MM/yyyy | +| Lithuania | Lithuanian | yyyy.M.d | +| Luxembourg | French | dd/MM/yyyy | +| Luxembourg | German | dd.MM.yyyy | +| Latvia | Latvian | yyyy.d.M | +| Morocco | Arabic | dd/MM/yyyy | +| Mexico | Spanish | d/MM/yyyy | +| Macedonia | Macedonian | d.M.yyyy | +| Malta | English | dd/MM/yyyy | +| Malta | Maltese | dd/MM/yyyy | +| Montenegro | Serbian | d.M.yyyy. | +| Malaysia | Malay | dd/MM/yyyy | +| Nicaragua | Spanish | MM-dd-yyyy | +| Netherlands | Dutch | d-M-yyyy | +| Norway | Norwegian | dd.MM.yyyy | +| Norway | Norwegian | dd.MM.yyyy | +| New Zealand | English | d/MM/yyyy | +| Oman | Arabic | dd/MM/yyyy | +| Panama | Spanish | MM/dd/yyyy | +| Peru | Spanish | dd/MM/yyyy | +| Philippines | English | M/d/yyyy | +| Poland | Polish | dd.MM.yyyy | +| Puerto Rico | Spanish | MM-dd-yyyy | +| Portugal | Portuguese | dd-MM-yyyy | +| Paraguay | Spanish | dd/MM/yyyy | +| Qatar | Arabic | dd/MM/yyyy | +| Romania | Romanian | dd.MM.yyyy | +| Russia | Russian | dd.MM.yyyy | +| Saudi Arabia | Arabic | dd/MM/yyyy | +| Serbia and Montenegro | Serbian | d.M.yyyy. | +| Sudan | Arabic | dd/MM/yyyy | +| Singapore | Chinese | dd/MM/yyyy | +| Singapore | English | M/d/yyyy | +| El Salvador | Spanish | MM-dd-yyyy | +| Serbia | Serbian | d.M.yyyy. | +| Slovakia | Slovak | d.M.yyyy | +| Slovenia | Slovenian | d.M.yyyy | +| Sweden | Swedish | yyyy-MM-dd | +| Syria | Arabic | dd/MM/yyyy | +| Thailand | Thai | d/M/2555 | +| Thailand | Thai | ๓/๖/๒๕๕๕ | +| Tunisia | Arabic | dd/MM/yyyy | +| Turkey | Turkish | dd.MM.yyyy | +| Taiwan | Chinese | yyyy/M/d | +| Ukraine | Ukrainian | dd.MM.yyyy | +| Uruguay | Spanish | dd/MM/yyyy | +| United States | English | M/d/yyyy | +| United States | Spanish | M/d/yyyy | +| Venezuela | Spanish | dd/MM/yyyy | +| Vietnam | Vietnamese | dd/MM/yyyy | +| Yemen | Arabic | dd/MM/yyyy | +| South Africa | English | yyyy/MM/dd | diff --git a/src/Config.ts b/src/Config.ts index 63c91c9e6c..5d94cb07e1 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -90,16 +90,23 @@ export interface ConfigParams { */ currencySymbol: string[], /** - * Sets date formats that are supported by date-parsing functions. + * Sets the date formats accepted by the date-parsing function. * - * The separator is ignored and can be any of the following: + * A format must be specified as a string consisting of tokens and separators. + * + * Supported tokes: + * - `DD` (day of month) + * - `MM` (month as a number) + * - `YYYY` (year as a 4-digit number) + * - `YY` (year as a 2-digit number) + * + * Supported separators: + * - `/` (slash) * - `-` (dash) + * - `.` (dot) * - ` ` (empty space) - * - `/` (slash) - * - * `YY` can be replaced with `YYYY`. * - * Any order of `YY`, `MM`, and `DD` is accepted as a date. + * Regardless of the separator specified in the format string, all of the above are accepted by the date-parsing function. * * @default ['DD/MM/YYYY', 'DD/MM/YY'] * @@ -214,7 +221,7 @@ export interface ConfigParams { /** * When set to `true`, function criteria require whole cells to match the pattern. * - * When set to `false`, function criteria require just a subword to match the pattern. + * When set to `false`, function criteria require just a sub-word to match the pattern. * * @default true * @category String @@ -275,7 +282,11 @@ export interface ConfigParams { */ nullYear: number, /** - * Sets a function that parses strings representing date-time into actual date-time. + * Sets a function that parses strings representing date-time into actual date-time values. + * + * The function should return a [DateTime](../globals.md#datetime) object or undefined. + * + * For more information, see the [Date and time handling guide](/guide/date-and-time-handling.md). * * @default defaultParseToDateTime * @@ -317,7 +328,11 @@ export interface ConfigParams { */ precisionRounding: number, /** - * Sets a function that converts date-time into strings. + * Sets a function that converts date-time values into strings. + * + * The function should return a string or undefined. + * + * For more information, see the [Date and time handling guide](/guide/date-and-time-handling.md). * * @default defaultStringifyDateTime * @@ -325,7 +340,11 @@ export interface ConfigParams { */ stringifyDateTime: (dateTime: SimpleDateTime, dateTimeFormat: string) => Maybe, /** - * Sets a function that converts time duration into strings. + * Sets a function that converts time duration values into strings. + * + * The function should return a string or undefined. + * + * For more information, see the [Date and time handling guide](/guide/date-and-time-handling.md). * * @default defaultStringifyDuration * @@ -343,7 +362,7 @@ export interface ConfigParams { */ smartRounding: boolean, /** - * Sets a thousands separator symbol for parsing numerical literals. + * Sets the thousands' separator symbol for parsing numerical literals. * * Can be one of the following: * - empty @@ -358,14 +377,16 @@ export interface ConfigParams { */ thousandSeparator: '' | ',' | ' ' | '.', /** - * Sets time formats that will be supported by time-parsing functions. + * Sets the time formats accepted by the time-parsing function. + * + * A format must be specified as a string consisting of at least two tokens separated by `:` (a colon). * - * The separator is `:` (colon). + * Supported tokes: + * - `hh` (hours) + * - `mm` (minutes) + * - `ss`, `ss.s`, `ss.ss`, `ss.sss`, `ss.ssss`, etc. (seconds) * - * Accepts any configuration of at least two of the following, in any order: - * - `hh`: hours - * - `mm`: minutes - * - `ss`: seconds + * The number of decimal places in the seconds token does not matter. All versions of the seconds token are equivalent in the context of parsing time values. * * @default ['hh:mm', 'hh:mm:ss.sss'] * @@ -519,7 +540,7 @@ export class Config implements ConfigParams, ParserConfig { /** @inheritDoc */ public readonly nullYear: number /** @inheritDoc */ - public readonly parseDateTime: (dateString: string, dateFormat?: string, timeFormat?: string) => Maybe + public readonly parseDateTime: (dateTimeString: string, dateFormat?: string, timeFormat?: string) => Maybe /** @inheritDoc */ public readonly stringifyDateTime: (date: SimpleDateTime, formatArg: string) => Maybe /** @inheritDoc */ diff --git a/src/DateTimeDefault.ts b/src/DateTimeDefault.ts index 3b593bcef7..3fc4afeaa8 100644 --- a/src/DateTimeDefault.ts +++ b/src/DateTimeDefault.ts @@ -46,9 +46,11 @@ export function defaultParseToDateTime(dateTimeString: string, dateFormat?: stri } } -export const secondsExtendedRegexp = /^ss\.(s+|0+)$/ +export const secondsExtendedRegexp = /^ss(\.(s+|0+))?$/ function defaultParseToTime(timeItems: string[], timeFormat: Maybe): Maybe { + const precision = 1000 + if (timeFormat === undefined) { return undefined } @@ -67,17 +69,14 @@ function defaultParseToTime(timeItems: string[], timeFormat: Maybe): May ampm = true timeItems.pop() } - let fractionOfSecondPrecision: number = 0 - if (formatItems.length >= 1 && secondsExtendedRegexp.test(formatItems[formatItems.length - 1])) { - fractionOfSecondPrecision = formatItems[formatItems.length - 1].length - 3 - formatItems[formatItems.length - 1] = 'ss' - } + if (timeItems.length !== formatItems.length) { return undefined } + const hourIndex = formatItems.indexOf('hh') const minuteIndex = formatItems.indexOf('mm') - const secondIndex = formatItems.indexOf('ss') + const secondIndex = formatItems.findIndex(item => secondsExtendedRegexp.test(item)) const hourString = hourIndex !== -1 ? timeItems[hourIndex] : '0' if (!/^\d+$/.test(hourString)) { @@ -104,8 +103,8 @@ function defaultParseToTime(timeItems: string[], timeFormat: Maybe): May if (!/^\d+(\.\d+)?$/.test(secondString)) { return undefined } - let seconds = Number(secondString) - seconds = Math.round(seconds * Math.pow(10, fractionOfSecondPrecision)) / Math.pow(10, fractionOfSecondPrecision) + + const seconds = Math.round(Number(secondString) * precision) / precision return {hours, minutes, seconds} } diff --git a/src/DateTimeHelper.ts b/src/DateTimeHelper.ts index b6d11380f3..fdebe36638 100644 --- a/src/DateTimeHelper.ts +++ b/src/DateTimeHelper.ts @@ -54,7 +54,7 @@ export class DateTimeHelper { private readonly minDateAbsoluteValue: number private readonly maxDateValue: number private readonly epochYearZero: number - private readonly parseDateTime: (dateString: string, dateFormat?: string, timeFormat?: string) => Maybe + private readonly parseDateTime: (dateTimeString: string, dateFormat?: string, timeFormat?: string) => Maybe private readonly leapYear1900: boolean constructor(private readonly config: Config) { diff --git a/src/format/format.ts b/src/format/format.ts index e3f5a376cc..349064dd1e 100644 --- a/src/format/format.ts +++ b/src/format/format.ts @@ -94,12 +94,6 @@ export function defaultStringifyDuration(time: SimpleTime, formatArg: string): M continue } - if (secondsExtendedRegexp.test(token.value)) { - const fractionOfSecondPrecision = token.value.length - 3 - result += (time.seconds < 10 ? '0' : '') + Math.floor(time.seconds * Math.pow(10, fractionOfSecondPrecision)) / Math.pow(10, fractionOfSecondPrecision) - continue - } - switch (token.value.toLowerCase()) { case 'h': case 'hh': { @@ -131,11 +125,16 @@ export function defaultStringifyDuration(time: SimpleTime, formatArg: string): M /* seconds */ case 's': case 'ss': { - result += padLeft(time.seconds, token.value.length) + result += padLeft(Math.floor(time.seconds), token.value.length) break } default: { + if (secondsExtendedRegexp.test(token.value)) { + const fractionOfSecondPrecision = Math.max(token.value.length - 3, 0) + result += (time.seconds < 10 ? '0' : '') + Math.floor(time.seconds * Math.pow(10, fractionOfSecondPrecision)) / Math.pow(10, fractionOfSecondPrecision) + continue + } return undefined } } @@ -162,12 +161,6 @@ export function defaultStringifyDateTime(dateTime: SimpleDateTime, formatArg: st continue } - if (secondsExtendedRegexp.test(token.value)) { - const fractionOfSecondPrecision = token.value.length - 3 - result += (dateTime.seconds < 10 ? '0' : '') + Math.floor(dateTime.seconds * Math.pow(10, fractionOfSecondPrecision)) / Math.pow(10, fractionOfSecondPrecision) - continue - } - switch (token.value.toLowerCase()) { /* hours*/ case 'h': @@ -224,6 +217,11 @@ export function defaultStringifyDateTime(dateTime: SimpleDateTime, formatArg: st break } default: { + if (secondsExtendedRegexp.test(token.value)) { + const fractionOfSecondPrecision = token.value.length - 3 + result += (dateTime.seconds < 10 ? '0' : '') + Math.floor(dateTime.seconds * Math.pow(10, fractionOfSecondPrecision)) / Math.pow(10, fractionOfSecondPrecision) + continue + } return undefined } } diff --git a/test/config.spec.ts b/test/config.spec.ts index 2fd06178c6..7b3fa51a2c 100644 --- a/test/config.spec.ts +++ b/test/config.spec.ts @@ -1,8 +1,9 @@ import {HyperFormula} from '../src' import {Config} from '../src/Config' import {enGB, plPL} from '../src/i18n/languages' -import {EmptyValue} from '../src/interpreter/InterpreterValue' -import {unregisterAllLanguages} from './testUtils' +import {EmptyValue, NumberType} from '../src/interpreter/InterpreterValue' +import {adr, unregisterAllLanguages} from './testUtils' +import {CellValueNoNumber} from '../src/Cell' describe('Config', () => { beforeEach(() => { @@ -120,7 +121,7 @@ describe('Config', () => { it('should throw error when currency symbol is not a string', () => { expect(() => { - new Config({currencySymbol: [42 as any]}) + new Config({currencySymbol: [ 42 as unknown as string ]}) }).toThrowError('Expected value of type: string[] for config parameter: currencySymbol') }) @@ -207,6 +208,115 @@ describe('Config', () => { expect(() => new Config({nullYear: 101})).toThrowError('Config parameter nullYear should be at most 100') }) + describe('#dateFormats', () => { + it('should use the data formats provided in config param', () => { + const dateFormats = ['DD/MM/YYYY'] + const engine = HyperFormula.buildFromArray([ + ['1'], + ['01/03/2022'], + ['2022/01/01'], + ], { dateFormats }) + expect(engine.getCellValueDetailedType(adr('A1'))).toEqual(NumberType.NUMBER_RAW) + expect(engine.getCellValueDetailedType(adr('A2'))).toEqual(NumberType.NUMBER_DATE) + expect(engine.getCellValueDetailedType(adr('A3'))).toEqual(CellValueNoNumber.STRING) + }) + + it('should parse the dates with different separators', () => { + const dateFormats = ['DD/MM/YYYY'] + const engine = HyperFormula.buildFromArray([[ + '01/03/2022', + '01-03-2022', + '01 03 2022', + '01.03.2022', + '01/03-2022', + '01 03.2022', + '01 03/2022', + '01.03-2022', + ]], { dateFormats }) + expect(engine.getCellValueDetailedType(adr('A1'))).toEqual(NumberType.NUMBER_DATE) + expect(engine.getCellValueFormat(adr('A1'))).toEqual('DD/MM/YYYY') + expect(engine.getCellValueDetailedType(adr('B1'))).toEqual(NumberType.NUMBER_DATE) + expect(engine.getCellValueFormat(adr('B1'))).toEqual('DD/MM/YYYY') + expect(engine.getCellValueDetailedType(adr('C1'))).toEqual(NumberType.NUMBER_DATE) + expect(engine.getCellValueFormat(adr('C1'))).toEqual('DD/MM/YYYY') + expect(engine.getCellValueDetailedType(adr('D1'))).toEqual(NumberType.NUMBER_DATE) + expect(engine.getCellValueFormat(adr('D1'))).toEqual('DD/MM/YYYY') + expect(engine.getCellValueDetailedType(adr('E1'))).toEqual(NumberType.NUMBER_DATE) + expect(engine.getCellValueFormat(adr('E1'))).toEqual('DD/MM/YYYY') + expect(engine.getCellValueDetailedType(adr('F1'))).toEqual(NumberType.NUMBER_DATE) + expect(engine.getCellValueFormat(adr('F1'))).toEqual('DD/MM/YYYY') + expect(engine.getCellValueDetailedType(adr('G1'))).toEqual(NumberType.NUMBER_DATE) + expect(engine.getCellValueFormat(adr('G1'))).toEqual('DD/MM/YYYY') + expect(engine.getCellValueDetailedType(adr('H1'))).toEqual(NumberType.NUMBER_DATE) + expect(engine.getCellValueFormat(adr('H1'))).toEqual('DD/MM/YYYY') + }) + }) + + describe('#timeFormats', () => { + it('should work with the "hh:mm" format', () => { + const timeFormats = ['hh:mm'] + const engine = HyperFormula.buildFromArray([ + ['13.33'], + ['13:33'], + ['01:33'], + ['1:33'], + ['13:33:33'], + ], { timeFormats }) + expect(engine.getCellValueDetailedType(adr('A1'))).toEqual(NumberType.NUMBER_RAW) + expect(engine.getCellValueDetailedType(adr('A2'))).toEqual(NumberType.NUMBER_TIME) + expect(engine.getCellValueDetailedType(adr('A3'))).toEqual(NumberType.NUMBER_TIME) + expect(engine.getCellValueDetailedType(adr('A4'))).toEqual(NumberType.NUMBER_TIME) + expect(engine.getCellValueDetailedType(adr('A5'))).toEqual(CellValueNoNumber.STRING) + }) + + it('should work with the "hh:mm:ss" format', () => { + const timeFormats = ['hh:mm:ss'] + const engine = HyperFormula.buildFromArray([ + ['13:33'], + ['13:33:00'], + ['01:33:33'], + ['1:33:33'], + ['13:33:33.3'], + ['13:33:33.33'], + ['13:33:33.333'], + ['13:33:33.3333'], + ['13:33:33.333333333333333333333333333333333333333333333333333333'], + ], { timeFormats }) + expect(engine.getCellValueDetailedType(adr('A1'))).toEqual(CellValueNoNumber.STRING) + expect(engine.getCellValueDetailedType(adr('A2'))).toEqual(NumberType.NUMBER_TIME) + expect(engine.getCellValueDetailedType(adr('A3'))).toEqual(NumberType.NUMBER_TIME) + expect(engine.getCellValueDetailedType(adr('A4'))).toEqual(NumberType.NUMBER_TIME) + expect(engine.getCellValueDetailedType(adr('A5'))).toEqual(NumberType.NUMBER_TIME) + expect(engine.getCellValueDetailedType(adr('A6'))).toEqual(NumberType.NUMBER_TIME) + expect(engine.getCellValueDetailedType(adr('A7'))).toEqual(NumberType.NUMBER_TIME) + expect(engine.getCellValueDetailedType(adr('A8'))).toEqual(NumberType.NUMBER_TIME) + expect(engine.getCellValueDetailedType(adr('A9'))).toEqual(NumberType.NUMBER_TIME) + }) + + it('the parsing result should be the same regardless of decimal places specified in format string', () => { + const dateAsString = '13:33:33.33333' + const dateAsNumber = 0.564969131944444 + + let engine = HyperFormula.buildFromArray([[dateAsString]], { timeFormats: ['hh:mm:ss'] }) + expect(engine.getCellValue(adr('A1'))).toEqual(dateAsNumber) + + engine = HyperFormula.buildFromArray([[dateAsString]], { timeFormats: ['hh:mm:ss.s'] }) + expect(engine.getCellValue(adr('A1'))).toEqual(dateAsNumber) + + engine = HyperFormula.buildFromArray([[dateAsString]], { timeFormats: ['hh:mm:ss.ss'] }) + expect(engine.getCellValue(adr('A1'))).toEqual(dateAsNumber) + + engine = HyperFormula.buildFromArray([[dateAsString]], { timeFormats: ['hh:mm:ss.sss'] }) + expect(engine.getCellValue(adr('A1'))).toEqual(dateAsNumber) + + engine = HyperFormula.buildFromArray([[dateAsString]], { timeFormats: ['hh:mm:ss.ssss'] }) + expect(engine.getCellValue(adr('A1'))).toEqual(dateAsNumber) + + engine = HyperFormula.buildFromArray([[dateAsString]], { timeFormats: ['hh:mm:ss.sssss'] }) + expect(engine.getCellValue(adr('A1'))).toEqual(dateAsNumber) + }) + }) + describe('deprecated option warning messages', () => { beforeEach(() => { spyOn(console, 'warn') diff --git a/test/interpreter/function-text.spec.ts b/test/interpreter/function-text.spec.ts index aad6d8e518..4efeace7f8 100644 --- a/test/interpreter/function-text.spec.ts +++ b/test/interpreter/function-text.spec.ts @@ -208,15 +208,23 @@ describe('time duration', () => { ['1.1', '=TEXT(A2, "[hh]:mm:ss")', ], ['0.1', '=TEXT(A3, "[mm]:ss")', ], ['1.1', '=TEXT(A4, "[mm]:ss")', ], - ['0.1111', '=TEXT(A5, "[mm]:ss.ss")', ], - ['0.1111', '=TEXT(A6, "[mm]:ss.00")', ], + ['1.1', '=TEXT(A5, "[hh]:m:ss")', ], + ['0.1111', '=TEXT(A6, "[mm]:ss.ss")', ], + ['0.1111', '=TEXT(A7, "[mm]:ss.00")', ], + ['0.1111', '=TEXT(A8, "hh:[mm]:s")', ], + ['0.1111', '=TEXT(A9, "h:[mm]")', ], + ['0.1111', '=TEXT(A10, "abc")', ], ]) expect(engine.getCellValue(adr('B1'))).toEqual('02:24:00') expect(engine.getCellValue(adr('B2'))).toEqual('26:24:00') expect(engine.getCellValue(adr('B3'))).toEqual('144:00') expect(engine.getCellValue(adr('B4'))).toEqual('1584:00') - expect(engine.getCellValue(adr('B5'))).toEqual('159:59.04') + expect(engine.getCellValue(adr('B5'))).toEqual('26:24:00') expect(engine.getCellValue(adr('B6'))).toEqual('159:59.04') + expect(engine.getCellValue(adr('B7'))).toEqual('159:59.04') + expect(engine.getCellValue(adr('B8'))).toEqual('02:39:59') + expect(engine.getCellValue(adr('B9'))).toEqual('2:39') + expect(engine.getCellValue(adr('B10'))).toEqual('abc') }) })