diff --git a/changelog.md b/changelog.md index adc35a0796..91b8f5bbdd 100644 --- a/changelog.md +++ b/changelog.md @@ -193,6 +193,8 @@ proc enumToString*(enums: openArray[enum]): string = - The switch ``-d:useWinAnsi`` is not supported anymore. +- In `times` module, procs `format` and `parse` accept a new optional `DateTimeLocale` argument for formatting/parsing dates in other languages. + ### Language additions diff --git a/lib/pure/times.nim b/lib/pure/times.nim index dc79877fdf..9312b7ba8b 100644 --- a/lib/pure/times.nim +++ b/lib/pure/times.nim @@ -282,6 +282,12 @@ type dFri = "Friday" dSat = "Saturday" dSun = "Sunday" + + DateTimeLocale* = object + MMM*: array[mJan..mDec, string] + MMMM*: array[mJan..mDec, string] + ddd*: array[dMon..dSun, string] + dddd*: array[dMon..dSun, string] MonthdayRange* = range[1..31] HourRange* = range[0..23] @@ -421,6 +427,13 @@ const unitWeights: array[FixedTimeUnit, int64] = [ 7 * secondsInDay * 1e9.int64, ] +const DefaultLocale* = DateTimeLocale( + MMM: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], + MMMM: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], + ddd: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], + dddd: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], +) + proc convert*[T: SomeInteger](unitFrom, unitTo: FixedTimeUnit, quantity: T): T {.inline.} = ## Convert a quantity of some duration unit to another duration unit. @@ -1894,7 +1907,7 @@ proc initTimeFormat*(format: string): TimeFormat = of tkPattern: result.patterns.add(stringToPattern(token).byte) -proc formatPattern(dt: DateTime, pattern: FormatPattern, result: var string) = +proc formatPattern(dt: DateTime, pattern: FormatPattern, result: var string, loc: DateTimeLocale) = template yearOfEra(dt: DateTime): int = if dt.year <= 0: abs(dt.year) + 1 else: dt.year @@ -1904,9 +1917,9 @@ proc formatPattern(dt: DateTime, pattern: FormatPattern, result: var string) = of dd: result.add dt.monthday.intToStr(2) of ddd: - result.add ($dt.weekday)[0..2] + result.add loc.ddd[dt.weekday] of dddd: - result.add $dt.weekday + result.add loc.dddd[dt.weekday] of h: result.add( if dt.hour == 0: "12" @@ -1932,9 +1945,9 @@ proc formatPattern(dt: DateTime, pattern: FormatPattern, result: var string) = of MM: result.add ord(dt.month).intToStr(2) of MMM: - result.add ($dt.month)[0..2] + result.add loc.MMM[dt.month] of MMMM: - result.add $dt.month + result.add loc.MMMM[dt.month] of s: result.add $dt.second of ss: @@ -2003,7 +2016,7 @@ proc formatPattern(dt: DateTime, pattern: FormatPattern, result: var string) = of Lit: assert false # Can't happen proc parsePattern(input: string, pattern: FormatPattern, i: var int, - parsed: var ParsedTime): bool = + parsed: var ParsedTime, loc: DateTimeLocale): bool = template takeInt(allowedWidth: Slice[int], allowSign = false): int = var sv: int var pd = parseInt(input, sv, i, allowedWidth.b, allowSign) @@ -2027,27 +2040,19 @@ proc parsePattern(input: string, pattern: FormatPattern, i: var int, parsed.monthday = some(monthday) result = monthday in MonthdayRange of ddd: - result = input.substr(i, i+2).toLowerAscii() in [ - "sun", "mon", "tue", "wed", "thu", "fri", "sat"] - if result: - i.inc 3 + result = false + for v in loc.ddd: + if input.substr(i, i+v.len-1).cmpIgnoreCase(v) == 0: + result = true + i.inc v.len + break of dddd: - if input.substr(i, i+5).cmpIgnoreCase("sunday") == 0: - i.inc 6 - elif input.substr(i, i+5).cmpIgnoreCase("monday") == 0: - i.inc 6 - elif input.substr(i, i+6).cmpIgnoreCase("tuesday") == 0: - i.inc 7 - elif input.substr(i, i+8).cmpIgnoreCase("wednesday") == 0: - i.inc 9 - elif input.substr(i, i+7).cmpIgnoreCase("thursday") == 0: - i.inc 8 - elif input.substr(i, i+5).cmpIgnoreCase("friday") == 0: - i.inc 6 - elif input.substr(i, i+7).cmpIgnoreCase("saturday") == 0: - i.inc 8 - else: - result = false + result = false + for v in loc.dddd: + if input.substr(i, i+v.len-1).cmpIgnoreCase(v) == 0: + result = true + i.inc v.len + break of h, H: parsed.hour = takeInt(1..2) result = parsed.hour in HourRange @@ -2069,62 +2074,21 @@ proc parsePattern(input: string, pattern: FormatPattern, i: var int, result = month in 1..12 parsed.month = some(month) of MMM: - case input.substr(i, i+2).toLowerAscii() - of "jan": parsed.month = some(1) - of "feb": parsed.month = some(2) - of "mar": parsed.month = some(3) - of "apr": parsed.month = some(4) - of "may": parsed.month = some(5) - of "jun": parsed.month = some(6) - of "jul": parsed.month = some(7) - of "aug": parsed.month = some(8) - of "sep": parsed.month = some(9) - of "oct": parsed.month = some(10) - of "nov": parsed.month = some(11) - of "dec": parsed.month = some(12) - else: - result = false - if result: - i.inc 3 + result = false + for n,v in loc.MMM: + if input.substr(i, i+v.len-1).cmpIgnoreCase(v) == 0: + result = true + i.inc v.len + parsed.month = some(n.int) + break of MMMM: - if input.substr(i, i+6).cmpIgnoreCase("january") == 0: - parsed.month = some(1) - i.inc 7 - elif input.substr(i, i+7).cmpIgnoreCase("february") == 0: - parsed.month = some(2) - i.inc 8 - elif input.substr(i, i+4).cmpIgnoreCase("march") == 0: - parsed.month = some(3) - i.inc 5 - elif input.substr(i, i+4).cmpIgnoreCase("april") == 0: - parsed.month = some(4) - i.inc 5 - elif input.substr(i, i+2).cmpIgnoreCase("may") == 0: - parsed.month = some(5) - i.inc 3 - elif input.substr(i, i+3).cmpIgnoreCase("june") == 0: - parsed.month = some(6) - i.inc 4 - elif input.substr(i, i+3).cmpIgnoreCase("july") == 0: - parsed.month = some(7) - i.inc 4 - elif input.substr(i, i+5).cmpIgnoreCase("august") == 0: - parsed.month = some(8) - i.inc 6 - elif input.substr(i, i+8).cmpIgnoreCase("september") == 0: - parsed.month = some(9) - i.inc 9 - elif input.substr(i, i+6).cmpIgnoreCase("october") == 0: - parsed.month = some(10) - i.inc 7 - elif input.substr(i, i+7).cmpIgnoreCase("november") == 0: - parsed.month = some(11) - i.inc 8 - elif input.substr(i, i+7).cmpIgnoreCase("december") == 0: - parsed.month = some(12) - i.inc 8 - else: - result = false + result = false + for n,v in loc.MMMM: + if input.substr(i, i+v.len-1).cmpIgnoreCase(v) == 0: + result = true + i.inc v.len + parsed.month = some(n.int) + break of s: parsed.second = takeInt(1..2) of ss: @@ -2294,7 +2258,7 @@ proc toDateTime(p: ParsedTime, zone: Timezone, f: TimeFormat, result.utcOffset = p.utcOffset.get() result = result.toTime.inZone(zone) -proc format*(dt: DateTime, f: TimeFormat): string {.raises: [].} = +proc format*(dt: DateTime, f: TimeFormat, loc: DateTimeLocale = DefaultLocale): string {.raises: [].} = ## Format ``dt`` using the format specified by ``f``. runnableExamples: let f = initTimeFormat("yyyy-MM-dd") @@ -2311,10 +2275,10 @@ proc format*(dt: DateTime, f: TimeFormat): string {.raises: [].} = result.add f.patterns[idx].char idx.inc else: - formatPattern(dt, f.patterns[idx].FormatPattern, result = result) + formatPattern(dt, f.patterns[idx].FormatPattern, result = result, loc = loc) idx.inc -proc format*(dt: DateTime, f: string): string +proc format*(dt: DateTime, f: string, loc: DateTimeLocale = DefaultLocale): string {.raises: [TimeFormatParseError].} = ## Shorthand for constructing a ``TimeFormat`` and using it to format ``dt``. ## @@ -2324,7 +2288,7 @@ proc format*(dt: DateTime, f: string): string let dt = initDateTime(01, mJan, 2000, 00, 00, 00, utc()) doAssert "2000-01-01" == format(dt, "yyyy-MM-dd") let dtFormat = initTimeFormat(f) - result = dt.format(dtFormat) + result = dt.format(dtFormat, loc) proc format*(dt: DateTime, f: static[string]): string {.raises: [].} = ## Overload that validates ``format`` at compile time. @@ -2358,12 +2322,14 @@ template formatValue*(result: var string; value: Time, specifier: string) = ## adapter for strformat. Not intended to be called directly. result.add format(value, specifier) -proc parse*(input: string, f: TimeFormat, zone: Timezone = local()): DateTime +proc parse*(input: string, f: TimeFormat, zone: Timezone = local(), loc: DateTimeLocale = DefaultLocale): DateTime {.raises: [TimeParseError, Defect].} = ## Parses ``input`` as a ``DateTime`` using the format specified by ``f``. ## If no UTC offset was parsed, then ``input`` is assumed to be specified in ## the ``zone`` timezone. If a UTC offset was parsed, the result will be ## converted to the ``zone`` timezone. + ## + ## Month and day names from the passed in ``loc`` are used. runnableExamples: let f = initTimeFormat("yyyy-MM-dd") let dt = initDateTime(01, mJan, 2000, 00, 00, 00, utc()) @@ -2385,7 +2351,7 @@ proc parse*(input: string, f: TimeFormat, zone: Timezone = local()): DateTime inpIdx.inc patIdx.inc else: - if not parsePattern(input, pattern, inpIdx, parsed): + if not parsePattern(input, pattern, inpIdx, parsed, loc): raiseParseException(f, input, "Failed on pattern '" & $pattern & "'") patIdx.inc @@ -2399,7 +2365,7 @@ proc parse*(input: string, f: TimeFormat, zone: Timezone = local()): DateTime result = toDateTime(parsed, zone, f, input) -proc parse*(input, f: string, tz: Timezone = local()): DateTime +proc parse*(input, f: string, tz: Timezone = local(), loc: DateTimeLocale = DefaultLocale): DateTime {.raises: [TimeParseError, TimeFormatParseError, Defect].} = ## Shorthand for constructing a ``TimeFormat`` and using it to parse ## ``input`` as a ``DateTime``. @@ -2410,13 +2376,13 @@ proc parse*(input, f: string, tz: Timezone = local()): DateTime let dt = initDateTime(01, mJan, 2000, 00, 00, 00, utc()) doAssert dt == parse("2000-01-01", "yyyy-MM-dd", utc()) let dtFormat = initTimeFormat(f) - result = input.parse(dtFormat, tz) + result = input.parse(dtFormat, tz, loc = loc) -proc parse*(input: string, f: static[string], zone: Timezone = local()): +proc parse*(input: string, f: static[string], zone: Timezone = local(), loc: DateTimeLocale = DefaultLocale): DateTime {.raises: [TimeParseError, Defect].} = ## Overload that validates ``f`` at compile time. const f2 = initTimeFormat(f) - result = input.parse(f2, zone) + result = input.parse(f2, zone, loc = loc) proc parseTime*(input, f: string, zone: Timezone): Time {.raises: [TimeParseError, TimeFormatParseError, Defect].} = diff --git a/tests/stdlib/ttimes.nim b/tests/stdlib/ttimes.nim index b29f5090b4..ef6712171a 100644 --- a/tests/stdlib/ttimes.nim +++ b/tests/stdlib/ttimes.nim @@ -452,6 +452,23 @@ suite "ttimes": doAssert dt.format("zz") == tz[2] doAssert dt.format("zzz") == tz[3] + test "format locale": + let loc = DateTimeLocale( + MMM: ["Fir","Sec","Thi","Fou","Fif","Six","Sev","Eig","Nin","Ten","Ele","Twe"], + MMMM: ["Firsty", "Secondy", "Thirdy", "Fourthy", "Fifthy", "Sixthy", "Seventhy", "Eighthy", "Ninthy", "Tenthy", "Eleventhy", "Twelfthy"], + ddd: ["Red", "Ora.", "Yel.", "Gre.", "Blu.", "Vio.", "Whi."], + dddd: ["Red", "Orange", "Yellow", "Green", "Blue", "Violet", "White"], + ) + var dt = initDateTime(5, mJan, 2010, 17, 01, 02, utc()) + check dt.format("d", loc) == "5" + check dt.format("dd", loc) == "05" + check dt.format("ddd", loc) == "Ora." + check dt.format("dddd", loc) == "Orange" + check dt.format("M", loc) == "1" + check dt.format("MM", loc) == "01" + check dt.format("MMM", loc) == "Fir" + check dt.format("MMMM", loc) == "Firsty" + test "parse": check $parse("20180101", "yyyyMMdd", utc()) == "2018-01-01T00:00:00Z" parseTestExcp("+120180101", "yyyyMMdd") @@ -473,6 +490,16 @@ suite "ttimes": parseTestExcp("2000 A", "yyyy g") + test "parse locale": + let loc = DateTimeLocale( + MMM: ["Fir","Sec","Thi","Fou","Fif","Six","Sev","Eig","Nin","Ten","Ele","Twe"], + MMMM: ["Firsty", "Secondy", "Thirdy", "Fourthy", "Fifthy", "Sixthy", "Seventhy", "Eighthy", "Ninthy", "Tenthy", "Eleventhy", "Twelfthy"], + ddd: ["Red", "Ora.", "Yel.", "Gre.", "Blu.", "Vio.", "Whi."], + dddd: ["Red", "Orange", "Yellow", "Green", "Blue", "Violet", "White"], + ) + check $parse("02 Fir 2019", "dd MMM yyyy", utc(), loc) == "2019-01-02T00:00:00Z" + check $parse("Fourthy 6, 2017", "MMMM d, yyyy", utc(), loc) == "2017-04-06T00:00:00Z" + test "countLeapYears": # 1920, 2004 and 2020 are leap years, and should be counted starting at the following year check countLeapYears(1920) + 1 == countLeapYears(1921)