Skip to content

Commit

Permalink
time parsing (#276)
Browse files Browse the repository at this point in the history
* refactor

* refactor

* refactor

* refactor

* small change to dates

* date refactor -> now simpler functions as args

* linter

* refactor of date printing

* test for custom printing

* .

* .

* more careful dateparsing

* safer default parsing

* .

* custom time parsing

* tests for time parsing

* minor test

* am/pm parsing and some tests

* linter

* formatted printing

* linter

* small fix

* refactor

* refactor

* .

* quick fix

* quickfix 2
  • Loading branch information
izulin committed Apr 6, 2020
1 parent 606be81 commit de7bbcc
Show file tree
Hide file tree
Showing 20 changed files with 415 additions and 259 deletions.
4 changes: 2 additions & 2 deletions src/BuildEngineFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {CellContentParser} from './CellContentParser'
import {Exporter} from './CellValue'
import {buildColumnSearchStrategy, ColumnSearchStrategy} from './ColumnSearch/ColumnSearchStrategy'
import {Config, ConfigParams} from './Config'
import {DateTimeHelper} from './DateTimeHelper'
import {CrudOperations} from './CrudOperations'
import {DateHelper} from './DateHelper'
import {DependencyGraph} from './DependencyGraph'
import {Evaluator} from './Evaluator'
import {GraphBuilder, Sheet, Sheets} from './GraphBuilder'
Expand Down Expand Up @@ -54,7 +54,7 @@ export class BuildEngineFactory {
const notEmpty = sheetMapping.numberOfSheets() > 0
const parser = new ParserWithCaching(config, notEmpty ? sheetMapping.get : sheetMapping.fetch)
const unparser = new Unparser(config, buildLexerConfig(config), sheetMapping.fetchDisplayName)
const dateHelper = new DateHelper(config)
const dateHelper = new DateTimeHelper(config)
const numberLiteralHelper = new NumberLiteralHelper(config)
const cellContentParser = new CellContentParser(config, dateHelper, numberLiteralHelper)

Expand Down
4 changes: 2 additions & 2 deletions src/CellContentParser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {CellError, EmptyValue, EmptyValueType, ErrorType} from './Cell'
import {Config} from './Config'
import {DateHelper} from './DateHelper'
import {DateTimeHelper} from './DateTimeHelper'
import {UnableToParse} from './errors'
import {fixNegativeZero, isNumberOverflow} from './interpreter/ArithmeticHelper'
import {NumberLiteralHelper} from './NumberLiteralHelper'
Expand Down Expand Up @@ -91,7 +91,7 @@ export function isError(text: string, errorMapping: Record<string, ErrorType>):
export class CellContentParser {
constructor(
private readonly config: Config,
private readonly dateHelper: DateHelper,
private readonly dateHelper: DateTimeHelper,
private readonly numberLiteralsHelper: NumberLiteralHelper) {
}

Expand Down
64 changes: 40 additions & 24 deletions src/Config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {ErrorType} from './Cell'
import {defaultParseToDate, instanceOfSimpleDate, SimpleDate} from './DateHelper'
import {defaultParseToDateTime} from './DateTimeDefault'
import {DateTime, instanceOfSimpleDate, SimpleDate, SimpleDateTime} from './DateTimeHelper'
import {ExpectedOneOfValues, ExpectedValueOfType} from './errors'
import {AlwaysDense, ChooseAddressMapping} from './DependencyGraph/AddressMapping/ChooseAddressMappingPolicy'
import {defaultStringifyDateTime} from './format/format'
import {HyperFormula} from './HyperFormula'
import {defaultPrintDate} from './format/format'
import {TranslationPackage} from './i18n'
import {AbsPlugin} from './interpreter/plugin/AbsPlugin'
import {BitShiftPlugin} from './interpreter/plugin/BitShiftPlugin'
Expand Down Expand Up @@ -91,15 +92,25 @@ export interface ConfigParams {
/**
* A list of date formats that are supported by date parsing functions.
*
* The separator is ignored and it can be any non-alpha-numeric symbol.
* The separator is ignored and it can be any of '-',' ','/'.
*
* Any configuration of YYYY, YY, MM, DD is accepted as a date, they can be put in any order, and any subset of those.
* Any order of YY, MM, DD is accepted as a date, and YY can be replaced with YYYY.
*
* @default ['MM/DD/YYYY', 'MM/DD/YY']
*
* @category Date
* @category DateTime
*/
dateFormats: string[],
/**
* A list of time formats that are supported by time parsing functions.
*
* The separator is ':'.
*
* Any configuration of at least two of hh, mm, ss is accepted as a time, and they can be put in any order.
*
* @default ['hh:mm', 'hh:mm:ss']
*/
timeFormats: string[],
/**
* A separator character used to separate arguments of procedures in formulas. Must be different from [[decimalSeparator]] and [[thousandSeparator]].
*
Expand Down Expand Up @@ -172,7 +183,7 @@ export interface ConfigParams {
*
* @default false
*
* @category Date
* @category DateTime
*/
leapYear1900: boolean,
/**
Expand Down Expand Up @@ -210,17 +221,17 @@ export interface ConfigParams {
*
* @default 30
*
* @category Date
* @category DateTime
*/
nullYear: number,
/**
* Allows to provide a function that takes a string representing date and parses it into an actual date.
* Allows to provide a function that takes a string representing date-time and parses it into an actual date-time.
*
* @default defaultParseToDate
* @default defaultParseToDateTime
*
* @category Date
* @category DateTime
*/
parseDate: (dateString: string, dateFormats: string) => Maybe<SimpleDate>,
parseDateTime: (dateTimeString: string, dateFormat: string, timeFormat: string) => Maybe<DateTime>,
/**
* Controls how far two numerical values need to be from each other to be treated as non-equal.
*
Expand Down Expand Up @@ -252,13 +263,13 @@ export interface ConfigParams {
*/
precisionRounding: number,
/**
* Allows to provide a function that takes date (represented as a number) and prints it into string.
* Allows to provide a function that takes date and prints it into string.
*
* @default defaultStringifyDate
* @default defaultStringifyDateTime
*
* @category Date
* @category DateTime
*/
stringifyDate: (date: SimpleDate, dateFormat: string) => Maybe<string>,
stringifyDateTime: (dateTime: SimpleDateTime, dateFormat: string) => Maybe<string>,
/**
* Sets the rounding.
*
Expand Down Expand Up @@ -310,7 +321,7 @@ export interface ConfigParams {
*
* @default {year: 1899, month: 12, day: 30}
*
* @category Date
* @category DateTime
*/
nullDate: SimpleDate,
}
Expand All @@ -326,6 +337,7 @@ export class Config implements ConfigParams, ParserConfig {
ignorePunctuation: false,
chooseAddressMappingPolicy: new AlwaysDense(),
dateFormats: ['MM/DD/YYYY', 'MM/DD/YY'],
timeFormats: ['hh:mm', 'hh:mm:ss'],
functionArgSeparator: ',',
decimalSeparator: '.',
thousandSeparator: '',
Expand All @@ -338,8 +350,8 @@ export class Config implements ConfigParams, ParserConfig {
matrixDetection: true,
matrixDetectionThreshold: 100,
nullYear: 30,
parseDate: defaultParseToDate,
stringifyDate: defaultPrintDate,
parseDateTime: defaultParseToDateTime,
stringifyDateTime: defaultStringifyDateTime,
precisionEpsilon: 1e-13,
precisionRounding: 14,
useColumnIndex: false,
Expand Down Expand Up @@ -395,6 +407,8 @@ export class Config implements ConfigParams, ParserConfig {
/** @inheritDoc */
public readonly dateFormats: string[]
/** @inheritDoc */
public readonly timeFormats: string[]
/** @inheritDoc */
public readonly functionArgSeparator: string
/** @inheritDoc */
public readonly decimalSeparator: '.' | ','
Expand All @@ -420,9 +434,9 @@ export class Config implements ConfigParams, ParserConfig {
/** @inheritDoc */
public readonly nullYear: number
/** @inheritDoc */
public readonly parseDate: (dateString: string, dateFormats: string) => Maybe<SimpleDate>
public readonly parseDateTime: (dateString: string, dateFormats: string) => Maybe<SimpleDateTime>
/** @inheritDoc */
public readonly stringifyDate: (date: SimpleDate, formatArg: string) => Maybe<string>
public readonly stringifyDateTime: (date: SimpleDateTime, formatArg: string) => Maybe<string>
/** @inheritDoc */
public readonly precisionEpsilon: number
/** @inheritDoc */
Expand Down Expand Up @@ -457,6 +471,7 @@ export class Config implements ConfigParams, ParserConfig {
caseFirst,
chooseAddressMappingPolicy,
dateFormats,
timeFormats,
functionArgSeparator,
decimalSeparator,
thousandSeparator,
Expand All @@ -470,8 +485,8 @@ export class Config implements ConfigParams, ParserConfig {
matrixDetection,
matrixDetectionThreshold,
nullYear,
parseDate,
stringifyDate,
parseDateTime,
stringifyDateTime,
precisionEpsilon,
precisionRounding,
useColumnIndex,
Expand All @@ -486,6 +501,7 @@ export class Config implements ConfigParams, ParserConfig {
this.ignorePunctuation = this.valueFromParam(ignorePunctuation, 'boolean', 'ignorePunctuation')
this.chooseAddressMappingPolicy = chooseAddressMappingPolicy ?? Config.defaultConfig.chooseAddressMappingPolicy
this.dateFormats = this.valueFromParamCheck(dateFormats, Array.isArray, 'array', 'dateFormats')
this.timeFormats = this.valueFromParamCheck(timeFormats, Array.isArray, 'array', 'timeFormats')
this.functionArgSeparator = this.valueFromParam(functionArgSeparator, 'string', 'functionArgSeparator')
this.decimalSeparator = this.valueFromParam(decimalSeparator, ['.', ','], 'decimalSeparator')
this.language = this.valueFromParam(language, 'string', 'language')
Expand All @@ -502,10 +518,10 @@ export class Config implements ConfigParams, ParserConfig {
this.useColumnIndex = this.valueFromParam(useColumnIndex, 'boolean', 'useColumnIndex')
this.useStats = this.valueFromParam(useStats, 'boolean', 'useStats')
this.vlookupThreshold = this.valueFromParam(vlookupThreshold, 'number', 'vlookupThreshold')
this.parseDateTime = this.valueFromParam(parseDateTime, 'function', 'parseDateTime')
this.stringifyDateTime = this.valueFromParam(stringifyDateTime, 'function', 'stringifyDateTime')
this.translationPackage = HyperFormula.getLanguage(this.language)
this.errorMapping = this.translationPackage.buildErrorMapping()
this.parseDate = this.valueFromParam(parseDate, 'function', 'parseDate')
this.stringifyDate = this.valueFromParam(stringifyDate, 'function', 'stringifyDate')
this.nullDate = this.valueFromParamCheck(nullDate, instanceOfSimpleDate, 'IDate', 'nullDate')
this.leapYear1900 = this.valueFromParam(leapYear1900, 'boolean', 'leapYear1900')

Expand Down
135 changes: 135 additions & 0 deletions src/DateTimeDefault.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {DateTime, SimpleDate, SimpleTime} from './DateTimeHelper'
import {Maybe} from './Maybe'

export function defaultParseToDateTime(dateTimeString: string, dateFormat: string, timeFormat: string): Maybe<DateTime> {
dateTimeString = dateTimeString.replace(/\s\s+/g, ' ').trim().toLowerCase()
let ampmtoken: string | undefined = dateTimeString.substring(dateTimeString.length - 2)
if (ampmtoken === 'am' || ampmtoken === 'pm') {
dateTimeString = dateTimeString.substring(0, dateTimeString.length - 2).trim()
} else {
ampmtoken = undefined
}
const dateItems = dateTimeString.split(/[ /.-]/g)
const timeItems = dateItems[dateItems.length - 1].split(':')
if (ampmtoken !== undefined) {
timeItems.push(ampmtoken)
}

if (dateItems.length === 1) {
return defaultParseToTime(timeItems, timeFormat)
}
if (timeItems.length === 1) {
return defaultParseToDate(dateItems, dateFormat)
}
const parsedDate = defaultParseToDate(dateItems.slice(0, dateItems.length - 1), dateFormat)
const parsedTime = defaultParseToTime(timeItems, timeFormat)
if (parsedDate === undefined) {
return undefined
} else if (parsedTime === undefined) {
return undefined
} else {
return {...parsedDate, ...parsedTime}
}
}

export function defaultParseToTime(timeItems: string[], timeFormat: string): Maybe<SimpleTime> {
timeFormat = timeFormat.toLowerCase()
if (timeFormat.length >= 1 && timeFormat.endsWith('a')) {
timeFormat = timeFormat.substring(0, timeFormat.length - 1).trim()
}
const formatItems = timeFormat.split(':')
let ampm = undefined
if (timeItems[timeItems.length - 1] === 'am') {
ampm = false
timeItems.pop()
} else if (timeItems[timeItems.length - 1] === 'pm') {
ampm = true
timeItems.pop()
}
if (timeItems.length !== formatItems.length) {
return undefined
}
const hourIndex = formatItems.indexOf('hh')
const minuteIndex = formatItems.indexOf('mm')
const secondIndex = formatItems.indexOf('ss')

const hourString = hourIndex !== -1 ? timeItems[hourIndex] : '0'
if (!/^\d+$/.test(hourString)) {
return undefined
}
let hour = Number(hourString)
if (ampm !== undefined) {
if (hour < 0 || hour > 12) {
return undefined
}
hour = hour % 12
if (ampm) {
hour = hour + 12
}
}

const minuteString = minuteIndex !== -1 ? timeItems[minuteIndex] : '0'
if (!/^\d+$/.test(minuteString)) {
return undefined
}
const minute = Number(minuteString)

const secondString = secondIndex !== -1 ? timeItems[secondIndex] : '0'
if (!/^\d+$/.test(secondString)) {
return undefined
}
const second = Number(secondString)

return {hour, minute, second}
}

export function defaultParseToDate(dateItems: string[], dateFormat: string): Maybe<SimpleDate> {
const formatItems = dateFormat.toLowerCase().split(/[ /.-]/g)
if (dateItems.length !== formatItems.length) {
return undefined
}
const monthIndex = formatItems.indexOf('mm')
const dayIndex = formatItems.indexOf('dd')
const yearIndexLong = formatItems.indexOf('yyyy')
const yearIndexShort = formatItems.indexOf('yy')
if (!(monthIndex in dateItems) || !(dayIndex in dateItems) ||
(!(yearIndexLong in dateItems) && !(yearIndexShort in dateItems))) {
return undefined
}
if (yearIndexLong in dateItems && yearIndexShort in dateItems) {
return undefined
}
let year
if (yearIndexLong in dateItems) {
const yearString = dateItems[yearIndexLong]
if (/^\d+$/.test(yearString)) {
year = Number(yearString)
if (year < 1000 || year > 9999) {
return undefined
}
} else {
return undefined
}
} else {
const yearString = dateItems[yearIndexShort]
if (/^\d+$/.test(yearString)) {
year = Number(yearString)
if (year < 0 || year > 99) {
return undefined
}
} else {
return undefined
}
}
const monthString = dateItems[monthIndex]
if (!/^\d+$/.test(monthString)) {
return undefined
}
const month = Number(monthString)
const dayString = dateItems[dayIndex]
if (!/^\d+$/.test(dayString)) {
return undefined
}
const day = Number(dayString)
return {year, month, day}
}

0 comments on commit de7bbcc

Please sign in to comment.