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')
})
})