Allow for locale-based parsing/formatting of dates (#11170)
* Allow for locale-based parsing/formatting of dates * Updates based on review feedback of PR 11170 DateTimeLocale arrays are now indexed by Month and WeekDay enums. More sane date used for testing. Documentation newline. Case change of DefaultLocale (and make it public) * Add changelog entry for DateTimeLocale addition to times module * Use pattern symbols for DateTimeLocale attribute names
This commit is contained in:
parent
072566d5f4
commit
8180d443b9
3 changed files with 86 additions and 91 deletions
|
|
@ -193,6 +193,8 @@ proc enumToString*(enums: openArray[enum]): string =
|
||||||
|
|
||||||
- The switch ``-d:useWinAnsi`` is not supported anymore.
|
- 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
|
### Language additions
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,12 @@ type
|
||||||
dFri = "Friday"
|
dFri = "Friday"
|
||||||
dSat = "Saturday"
|
dSat = "Saturday"
|
||||||
dSun = "Sunday"
|
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]
|
MonthdayRange* = range[1..31]
|
||||||
HourRange* = range[0..23]
|
HourRange* = range[0..23]
|
||||||
|
|
@ -421,6 +427,13 @@ const unitWeights: array[FixedTimeUnit, int64] = [
|
||||||
7 * secondsInDay * 1e9.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
|
proc convert*[T: SomeInteger](unitFrom, unitTo: FixedTimeUnit, quantity: T): T
|
||||||
{.inline.} =
|
{.inline.} =
|
||||||
## Convert a quantity of some duration unit to another duration unit.
|
## Convert a quantity of some duration unit to another duration unit.
|
||||||
|
|
@ -1894,7 +1907,7 @@ proc initTimeFormat*(format: string): TimeFormat =
|
||||||
of tkPattern:
|
of tkPattern:
|
||||||
result.patterns.add(stringToPattern(token).byte)
|
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 =
|
template yearOfEra(dt: DateTime): int =
|
||||||
if dt.year <= 0: abs(dt.year) + 1 else: dt.year
|
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:
|
of dd:
|
||||||
result.add dt.monthday.intToStr(2)
|
result.add dt.monthday.intToStr(2)
|
||||||
of ddd:
|
of ddd:
|
||||||
result.add ($dt.weekday)[0..2]
|
result.add loc.ddd[dt.weekday]
|
||||||
of dddd:
|
of dddd:
|
||||||
result.add $dt.weekday
|
result.add loc.dddd[dt.weekday]
|
||||||
of h:
|
of h:
|
||||||
result.add(
|
result.add(
|
||||||
if dt.hour == 0: "12"
|
if dt.hour == 0: "12"
|
||||||
|
|
@ -1932,9 +1945,9 @@ proc formatPattern(dt: DateTime, pattern: FormatPattern, result: var string) =
|
||||||
of MM:
|
of MM:
|
||||||
result.add ord(dt.month).intToStr(2)
|
result.add ord(dt.month).intToStr(2)
|
||||||
of MMM:
|
of MMM:
|
||||||
result.add ($dt.month)[0..2]
|
result.add loc.MMM[dt.month]
|
||||||
of MMMM:
|
of MMMM:
|
||||||
result.add $dt.month
|
result.add loc.MMMM[dt.month]
|
||||||
of s:
|
of s:
|
||||||
result.add $dt.second
|
result.add $dt.second
|
||||||
of ss:
|
of ss:
|
||||||
|
|
@ -2003,7 +2016,7 @@ proc formatPattern(dt: DateTime, pattern: FormatPattern, result: var string) =
|
||||||
of Lit: assert false # Can't happen
|
of Lit: assert false # Can't happen
|
||||||
|
|
||||||
proc parsePattern(input: string, pattern: FormatPattern, i: var int,
|
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 =
|
template takeInt(allowedWidth: Slice[int], allowSign = false): int =
|
||||||
var sv: int
|
var sv: int
|
||||||
var pd = parseInt(input, sv, i, allowedWidth.b, allowSign)
|
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)
|
parsed.monthday = some(monthday)
|
||||||
result = monthday in MonthdayRange
|
result = monthday in MonthdayRange
|
||||||
of ddd:
|
of ddd:
|
||||||
result = input.substr(i, i+2).toLowerAscii() in [
|
result = false
|
||||||
"sun", "mon", "tue", "wed", "thu", "fri", "sat"]
|
for v in loc.ddd:
|
||||||
if result:
|
if input.substr(i, i+v.len-1).cmpIgnoreCase(v) == 0:
|
||||||
i.inc 3
|
result = true
|
||||||
|
i.inc v.len
|
||||||
|
break
|
||||||
of dddd:
|
of dddd:
|
||||||
if input.substr(i, i+5).cmpIgnoreCase("sunday") == 0:
|
result = false
|
||||||
i.inc 6
|
for v in loc.dddd:
|
||||||
elif input.substr(i, i+5).cmpIgnoreCase("monday") == 0:
|
if input.substr(i, i+v.len-1).cmpIgnoreCase(v) == 0:
|
||||||
i.inc 6
|
result = true
|
||||||
elif input.substr(i, i+6).cmpIgnoreCase("tuesday") == 0:
|
i.inc v.len
|
||||||
i.inc 7
|
break
|
||||||
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
|
|
||||||
of h, H:
|
of h, H:
|
||||||
parsed.hour = takeInt(1..2)
|
parsed.hour = takeInt(1..2)
|
||||||
result = parsed.hour in HourRange
|
result = parsed.hour in HourRange
|
||||||
|
|
@ -2069,62 +2074,21 @@ proc parsePattern(input: string, pattern: FormatPattern, i: var int,
|
||||||
result = month in 1..12
|
result = month in 1..12
|
||||||
parsed.month = some(month)
|
parsed.month = some(month)
|
||||||
of MMM:
|
of MMM:
|
||||||
case input.substr(i, i+2).toLowerAscii()
|
result = false
|
||||||
of "jan": parsed.month = some(1)
|
for n,v in loc.MMM:
|
||||||
of "feb": parsed.month = some(2)
|
if input.substr(i, i+v.len-1).cmpIgnoreCase(v) == 0:
|
||||||
of "mar": parsed.month = some(3)
|
result = true
|
||||||
of "apr": parsed.month = some(4)
|
i.inc v.len
|
||||||
of "may": parsed.month = some(5)
|
parsed.month = some(n.int)
|
||||||
of "jun": parsed.month = some(6)
|
break
|
||||||
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
|
|
||||||
of MMMM:
|
of MMMM:
|
||||||
if input.substr(i, i+6).cmpIgnoreCase("january") == 0:
|
result = false
|
||||||
parsed.month = some(1)
|
for n,v in loc.MMMM:
|
||||||
i.inc 7
|
if input.substr(i, i+v.len-1).cmpIgnoreCase(v) == 0:
|
||||||
elif input.substr(i, i+7).cmpIgnoreCase("february") == 0:
|
result = true
|
||||||
parsed.month = some(2)
|
i.inc v.len
|
||||||
i.inc 8
|
parsed.month = some(n.int)
|
||||||
elif input.substr(i, i+4).cmpIgnoreCase("march") == 0:
|
break
|
||||||
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
|
|
||||||
of s:
|
of s:
|
||||||
parsed.second = takeInt(1..2)
|
parsed.second = takeInt(1..2)
|
||||||
of ss:
|
of ss:
|
||||||
|
|
@ -2294,7 +2258,7 @@ proc toDateTime(p: ParsedTime, zone: Timezone, f: TimeFormat,
|
||||||
result.utcOffset = p.utcOffset.get()
|
result.utcOffset = p.utcOffset.get()
|
||||||
result = result.toTime.inZone(zone)
|
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``.
|
## Format ``dt`` using the format specified by ``f``.
|
||||||
runnableExamples:
|
runnableExamples:
|
||||||
let f = initTimeFormat("yyyy-MM-dd")
|
let f = initTimeFormat("yyyy-MM-dd")
|
||||||
|
|
@ -2311,10 +2275,10 @@ proc format*(dt: DateTime, f: TimeFormat): string {.raises: [].} =
|
||||||
result.add f.patterns[idx].char
|
result.add f.patterns[idx].char
|
||||||
idx.inc
|
idx.inc
|
||||||
else:
|
else:
|
||||||
formatPattern(dt, f.patterns[idx].FormatPattern, result = result)
|
formatPattern(dt, f.patterns[idx].FormatPattern, result = result, loc = loc)
|
||||||
idx.inc
|
idx.inc
|
||||||
|
|
||||||
proc format*(dt: DateTime, f: string): string
|
proc format*(dt: DateTime, f: string, loc: DateTimeLocale = DefaultLocale): string
|
||||||
{.raises: [TimeFormatParseError].} =
|
{.raises: [TimeFormatParseError].} =
|
||||||
## Shorthand for constructing a ``TimeFormat`` and using it to format ``dt``.
|
## 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())
|
let dt = initDateTime(01, mJan, 2000, 00, 00, 00, utc())
|
||||||
doAssert "2000-01-01" == format(dt, "yyyy-MM-dd")
|
doAssert "2000-01-01" == format(dt, "yyyy-MM-dd")
|
||||||
let dtFormat = initTimeFormat(f)
|
let dtFormat = initTimeFormat(f)
|
||||||
result = dt.format(dtFormat)
|
result = dt.format(dtFormat, loc)
|
||||||
|
|
||||||
proc format*(dt: DateTime, f: static[string]): string {.raises: [].} =
|
proc format*(dt: DateTime, f: static[string]): string {.raises: [].} =
|
||||||
## Overload that validates ``format`` at compile time.
|
## 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.
|
## adapter for strformat. Not intended to be called directly.
|
||||||
result.add format(value, specifier)
|
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].} =
|
{.raises: [TimeParseError, Defect].} =
|
||||||
## Parses ``input`` as a ``DateTime`` using the format specified by ``f``.
|
## 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
|
## 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
|
## the ``zone`` timezone. If a UTC offset was parsed, the result will be
|
||||||
## converted to the ``zone`` timezone.
|
## converted to the ``zone`` timezone.
|
||||||
|
##
|
||||||
|
## Month and day names from the passed in ``loc`` are used.
|
||||||
runnableExamples:
|
runnableExamples:
|
||||||
let f = initTimeFormat("yyyy-MM-dd")
|
let f = initTimeFormat("yyyy-MM-dd")
|
||||||
let dt = initDateTime(01, mJan, 2000, 00, 00, 00, utc())
|
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
|
inpIdx.inc
|
||||||
patIdx.inc
|
patIdx.inc
|
||||||
else:
|
else:
|
||||||
if not parsePattern(input, pattern, inpIdx, parsed):
|
if not parsePattern(input, pattern, inpIdx, parsed, loc):
|
||||||
raiseParseException(f, input, "Failed on pattern '" & $pattern & "'")
|
raiseParseException(f, input, "Failed on pattern '" & $pattern & "'")
|
||||||
patIdx.inc
|
patIdx.inc
|
||||||
|
|
||||||
|
|
@ -2399,7 +2365,7 @@ proc parse*(input: string, f: TimeFormat, zone: Timezone = local()): DateTime
|
||||||
|
|
||||||
result = toDateTime(parsed, zone, f, input)
|
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].} =
|
{.raises: [TimeParseError, TimeFormatParseError, Defect].} =
|
||||||
## Shorthand for constructing a ``TimeFormat`` and using it to parse
|
## Shorthand for constructing a ``TimeFormat`` and using it to parse
|
||||||
## ``input`` as a ``DateTime``.
|
## ``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())
|
let dt = initDateTime(01, mJan, 2000, 00, 00, 00, utc())
|
||||||
doAssert dt == parse("2000-01-01", "yyyy-MM-dd", utc())
|
doAssert dt == parse("2000-01-01", "yyyy-MM-dd", utc())
|
||||||
let dtFormat = initTimeFormat(f)
|
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].} =
|
DateTime {.raises: [TimeParseError, Defect].} =
|
||||||
## Overload that validates ``f`` at compile time.
|
## Overload that validates ``f`` at compile time.
|
||||||
const f2 = initTimeFormat(f)
|
const f2 = initTimeFormat(f)
|
||||||
result = input.parse(f2, zone)
|
result = input.parse(f2, zone, loc = loc)
|
||||||
|
|
||||||
proc parseTime*(input, f: string, zone: Timezone): Time
|
proc parseTime*(input, f: string, zone: Timezone): Time
|
||||||
{.raises: [TimeParseError, TimeFormatParseError, Defect].} =
|
{.raises: [TimeParseError, TimeFormatParseError, Defect].} =
|
||||||
|
|
|
||||||
|
|
@ -452,6 +452,23 @@ suite "ttimes":
|
||||||
doAssert dt.format("zz") == tz[2]
|
doAssert dt.format("zz") == tz[2]
|
||||||
doAssert dt.format("zzz") == tz[3]
|
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":
|
test "parse":
|
||||||
check $parse("20180101", "yyyyMMdd", utc()) == "2018-01-01T00:00:00Z"
|
check $parse("20180101", "yyyyMMdd", utc()) == "2018-01-01T00:00:00Z"
|
||||||
parseTestExcp("+120180101", "yyyyMMdd")
|
parseTestExcp("+120180101", "yyyyMMdd")
|
||||||
|
|
@ -473,6 +490,16 @@ suite "ttimes":
|
||||||
|
|
||||||
parseTestExcp("2000 A", "yyyy g")
|
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":
|
test "countLeapYears":
|
||||||
# 1920, 2004 and 2020 are leap years, and should be counted starting at the following year
|
# 1920, 2004 and 2020 are leap years, and should be counted starting at the following year
|
||||||
check countLeapYears(1920) + 1 == countLeapYears(1921)
|
check countLeapYears(1920) + 1 == countLeapYears(1921)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue