diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index 0acbf9a082..509cfd15fa 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -9,13 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -#### All +* [GH-3769](https://github.com/fable-compiler/Fable/pull/3769) [All] Local plugin build does not run indefinably. (by @nojaf) +* [GH-3769](https://github.com/fable-compiler/Fable/pull/3769) [JS/TS] Types hidden by signature files should not be exported. (by @nojaf) +* [GH-3772](https://github.com/fable-compiler/Fable/pull/3772) [JS/TS] Re-implement `DateTime.ToString` custom format handling (by @MangelMaxime) -* [GH-3769](https://github.com/fable-compiler/Fable/pull/3769) Local plugin build does not run indefinably. (by @nojaf) + It now supports all custom format specifiers, and behave as if `CultureInfo.InvariantCulture` was used (Fable does not support Globalization). +* [GH-3772](https://github.com/fable-compiler/Fable/pull/3772) [JS/TS] Make compilation fails if calling `DateTime` constructor with microseconds (by @MangelMaxime) -#### JavaScript + JavaScript `Date` does not support microseconds, we need to wait for `Temporal` to be widely supported before reconsidering this. + +### Changed -* [GH-3769](https://github.com/fable-compiler/Fable/pull/3769) Types hidden by signature files should not be exported. (by @nojaf) +* [GH-3772](https://github.com/fable-compiler/Fable/pull/3772) [JS/TS] Split replacement for `DateTime` and `DateTimeOffset` (by @MangelMaxime) ## 4.13.0 - 2024-02-13 diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 0e23c6219e..5eeeabe1ad 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -2840,15 +2840,8 @@ let ignoreFormatProvider meth args = | "TryParse", input :: _culture :: defVal :: _ -> [ input; defVal ] | _ -> args -let dates (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - let getTime (e: Expr) = - Helper.InstanceCall(e, "getTime", t, []) - - let moduleName = - if i.DeclaringEntityFullName = Types.datetime then - "Date" - else - "DateOffset" +let dateTime (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + let moduleName = "Date" match i.CompiledName with | ".ctor" -> @@ -2857,9 +2850,6 @@ let dates (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio | ExprType(Number(Int64, _)) :: _ -> Helper.LibCall(com, moduleName, "fromTicks", t, args, i.SignatureArgTypes, ?loc = r) |> Some - | ExprType(DeclaredType(e, [])) :: _ when e.FullName = Types.datetime -> - Helper.LibCall(com, "DateOffset", "fromDate", t, args, i.SignatureArgTypes, ?loc = r) - |> Some | _ -> let last = List.last args @@ -2870,25 +2860,74 @@ let dates (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio let argTypes = (List.take 6 i.SignatureArgTypes) @ [ Int32.Number; last.Type ] Helper.LibCall(com, "Date", "create", t, args, argTypes, ?loc = r) |> Some + + // JavaScript Date doesn't support microseconds precision + | 8, Number(Int32, NumberInfo.Empty) -> + "JavaScript Date with doesn't support microseconds precision" + |> addError com ctx.InlinePath r + + None + // | 8, Number(_, NumberInfo.IsEnum ent) when ent.FullName = "System.DateTimeKind" -> + // "JavaScript Date with doesn't support microseconds precision" |> addError com ctx.InlinePath r + // None + | _ -> Helper.LibCall(com, moduleName, "create", t, args, i.SignatureArgTypes, ?loc = r) |> Some | "ToString" -> Helper.LibCall(com, "Date", "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some - | "get_Kind" - | "get_Offset" as meth -> - let moduleName = - if meth = "get_Kind" then - "Date" - else - "DateOffset" + // | "get_Kind" -> + // Helper.LibCall(com, moduleName, "kind", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) + // |> Some + // let y = DateTime.Now.UtcTicks + // let x = DateTimeOffset.Now.UtcTicks + // failwith "Not implemented" + | "get_Ticks" -> + Helper.LibCall(com, "Date", "getTicks", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) + |> Some + | meth -> + let args = ignoreFormatProvider meth args let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst - Helper.LibCall(com, moduleName, meth, t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) + Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + |> Some + + +let dateTimeOffset (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + let moduleName = "DateOffset" + + match i.CompiledName with + | ".ctor" -> + match args with + | [] -> Helper.LibCall(com, moduleName, "minValue", t, [], [], ?loc = r) |> Some + | ExprType(Number(Int64, _)) :: _ -> + Helper.LibCall(com, moduleName, "fromTicks", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | ExprType(DeclaredType(e, [])) :: _ when e.FullName = Types.datetime -> + Helper.LibCall(com, moduleName, "fromDate", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | _ -> + let last = List.last args + + match args.Length, last.Type with + | 7, Number(_, NumberInfo.IsEnum ent) when ent.FullName = "System.DateTimeKind" -> + let args = (List.take 6 args) @ [ makeIntConst 0; last ] + + let argTypes = (List.take 6 i.SignatureArgTypes) @ [ Int32.Number; last.Type ] + + Helper.LibCall(com, "Date", "create", t, args, argTypes, ?loc = r) |> Some + + | _ -> + Helper.LibCall(com, moduleName, "create", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "ToString" -> + Helper.LibCall(com, "Date", "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + |> Some + | "get_Offset" -> + Helper.LibCall(com, moduleName, "offset", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) |> Some - // DateTimeOffset | "get_LocalDateTime" -> Helper.LibCall(com, "DateOffset", "toLocalTime", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) |> Some @@ -2896,7 +2935,7 @@ let dates (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio Helper.LibCall(com, "DateOffset", "toUniversalTime", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) |> Some | "get_DateTime" -> - let kind = System.DateTimeKind.Unspecified |> int |> makeIntConst + let kind = DateTimeKind.Unspecified |> int |> makeIntConst Helper.LibCall( com, @@ -2921,6 +2960,7 @@ let dates (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some + let dateOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName with | ".ctor" when args.Length = 4 -> @@ -3878,8 +3918,8 @@ let private replacedModules = "System.Console", console "System.Diagnostics.Debug", debug "System.Diagnostics.Debugger", debug - Types.datetime, dates - Types.datetimeOffset, dates + Types.datetime, dateTime + Types.datetimeOffset, dateTimeOffset Types.dateOnly, dateOnly Types.timeOnly, timeOnly Types.timespan, timeSpans @@ -4054,11 +4094,11 @@ let tryType typ = | Builtin kind -> match kind with | BclGuid -> Some(Types.guid, guids, []) - | BclDateTime -> Some(Types.datetime, dates, []) - | BclDateTimeOffset -> Some(Types.datetimeOffset, dates, []) + | BclDateTime -> Some(Types.datetime, dateTime, []) + | BclDateTimeOffset -> Some(Types.datetimeOffset, dateTimeOffset, []) | BclDateOnly -> Some(Types.dateOnly, dateOnly, []) | BclTimeOnly -> Some(Types.timeOnly, timeOnly, []) - | BclTimer -> Some("System.Timers.Timer", timers, []) + | BclTimer -> Some(Types.timer, timers, []) | BclTimeSpan -> Some(Types.timespan, timeSpans, []) | BclHashSet genArg -> Some(Types.hashset, hashSets, [ genArg ]) | BclDictionary(key, value) -> Some(Types.dictionary, dictionaries, [ key; value ]) diff --git a/src/Fable.Transforms/Transforms.Util.fs b/src/Fable.Transforms/Transforms.Util.fs index cd6e606063..0da5b2be76 100644 --- a/src/Fable.Transforms/Transforms.Util.fs +++ b/src/Fable.Transforms/Transforms.Util.fs @@ -204,6 +204,9 @@ module Types = [] let timeOnly = "System.TimeOnly" + [] + let timer = "System.Timers.Timer" + [] let int8 = "System.SByte" diff --git a/src/fable-library-ts/Date.ts b/src/fable-library-ts/Date.ts index d06fdbf7ce..02e5da1216 100644 --- a/src/fable-library-ts/Date.ts +++ b/src/fable-library-ts/Date.ts @@ -15,6 +15,360 @@ import { compareDates, DateKind, dateOffset, IDateTime, IDateTimeOffset, padWith export type OffsetInMinutes = number; export type Offset = "Z" | OffsetInMinutes | null; +const shortDays = + [ + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" + ] + +const longDays = + [ + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" + ] + +const shortMonths = + [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" + ] + +const longMonths = + [ + "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" + ] + + +function parseRepeatToken(format: string, pos: number, patternChar: string) { + let tokenLength = 0; + let internalPos = pos; + while (internalPos < format.length && format[internalPos] === patternChar) { + internalPos++; + tokenLength++; + } + + return tokenLength; +} + +function parseNextChar(format: string, pos: number) { + if (pos >= format.length - 1) { + return -1 + } + + return format.charCodeAt(pos + 1); +} + +function parseQuotedString(format: string, pos: number) : [string, number] { + let beginPos = pos; + // Get the character used to quote the string + const quoteChar = format[pos]; + + let result = ""; + let foundQuote = false; + + while (pos < format.length) { + pos++; + const currentChar = format[pos]; + if (currentChar === quoteChar) { + foundQuote = true; + break; + } else if (currentChar === "\\") { + if (pos < format.length) { + pos++; + result += format[pos]; + } else { + // This means that '\' is the last character in the string. + throw new Error("Invalid string format"); + } + } else { + result += currentChar; + } + } + + if (!foundQuote) { + // We could not find the matching quote + throw new Error(`Invalid string format could not find matching quote for ${quoteChar}`); + } + + return [result, pos - beginPos + 1]; +} + +function dateToStringWithCustomFormat(date: Date, format: string, utc: boolean) { + let cursorPos = 0; + let tokenLength = 0; + let result = ""; + const localizedDate = utc ? DateTime(date.getTime(), DateKind.UTC) : date; + + while (cursorPos < format.length) { + const token = format[cursorPos]; + + switch (token) { + case "d": + tokenLength = parseRepeatToken(format, cursorPos, "d"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += localizedDate.getDate(); + break; + case 2: + result += padWithZeros(localizedDate.getDate(), 2); + break; + case 3: + result += shortDays[dayOfWeek(localizedDate)]; + break; + case 4: + result += longDays[dayOfWeek(localizedDate)]; + break; + default: + break; + } + break; + case "f": + tokenLength = parseRepeatToken(format, cursorPos, "f"); + cursorPos += tokenLength; + if (tokenLength <= 3) { + const precision = 10 ** (3 - tokenLength); + result += padWithZeros(Math.floor(millisecond(localizedDate) / precision), tokenLength); + } else if (tokenLength <= 7) { + // JavaScript Date only support precision to the millisecond + // so we fill the rest of the precision with 0 as if the date didn't have + // milliseconds provided to it. + // This is to have the same behavior as .NET when doing: + // DateTime(1, 2, 3, 4, 5, 6, DateTimeKind.Utc).ToString("fffff") => 00000 + result += (""+millisecond(localizedDate)).padEnd(tokenLength, "0"); + } + break; + case "F": + tokenLength = parseRepeatToken(format, cursorPos, "F"); + cursorPos += tokenLength; + if (tokenLength <= 3) { + const precision = 10 ** (3 - tokenLength); + const value = Math.floor(millisecond(localizedDate) / precision) + if (value != 0) { + result += padWithZeros(value, tokenLength); + } + } else if (tokenLength <= 7) { + // JavaScript Date only support precision to the millisecond + // so we can't go beyond that. + // We also need to pad start with 0 if the value is not 0 + const value = millisecond(localizedDate); + if (value != 0) { + result += padWithZeros(value, 3); + } + } + break; + case "g": + tokenLength = parseRepeatToken(format, cursorPos, "g"); + cursorPos += tokenLength; + if (tokenLength <= 2) { + result += "A.D."; + } + break; + case "h": + tokenLength = parseRepeatToken(format, cursorPos, "h"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += hour(localizedDate) % 12 + break; + case 2: + result += padWithZeros(hour(localizedDate) % 12, 2); + break; + default: + break; + } + break; + case "H": + tokenLength = parseRepeatToken(format, cursorPos, "H"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += hour(localizedDate); + break; + case 2: + result += padWithZeros(hour(localizedDate), 2); + break; + default: + break; + } + break; + case "K": + tokenLength = parseRepeatToken(format, cursorPos, "K"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + switch (kind(localizedDate)) { + case DateKind.UTC: + result += "Z"; + break; + case DateKind.Local: + result += dateOffsetToString(localizedDate.getTimezoneOffset() * -60000); + break; + case DateKind.Unspecified: + break; + } + break; + default: + break; + } + break; + case "m": + tokenLength = parseRepeatToken(format, cursorPos, "m"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += minute(localizedDate); + break; + case 2: + result += padWithZeros(minute(localizedDate), 2); + break; + default: + break; + } + break; + case "M": + tokenLength = parseRepeatToken(format, cursorPos, "M"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += month(localizedDate); + break; + case 2: + result += padWithZeros(month(localizedDate), 2); + break; + case 3: + result += shortMonths[month(localizedDate) - 1]; + break; + case 4: + result += longMonths[month(localizedDate) - 1]; + break; + default: + break; + } + break; + case "s": + tokenLength = parseRepeatToken(format, cursorPos, "s"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += second(localizedDate); + break; + case 2: + result += padWithZeros(second(localizedDate), 2); + break; + default: + break; + } + break; + case "t": + tokenLength = parseRepeatToken(format, cursorPos, "t"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += localizedDate.getHours() < 12 ? "A" : "P"; + break; + case 2: + result += localizedDate.getHours() < 12 ? "AM" : "PM"; + break; + default: + break; + } + break; + case "y": + tokenLength = parseRepeatToken(format, cursorPos, "y"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += localizedDate.getFullYear() % 100; + break; + case 2: + result += padWithZeros(localizedDate.getFullYear() % 100, 2); + break; + case 3: + result += padWithZeros(localizedDate.getFullYear(), 3); + break; + case 4: + result += padWithZeros(localizedDate.getFullYear(), 4); + break; + case 5: + result += padWithZeros(localizedDate.getFullYear(), 5); + break; + default: + break; + } + break; + case "z": + tokenLength = parseRepeatToken(format, cursorPos, "z"); + cursorPos += tokenLength; + let utcOffsetText = ""; + + switch (kind(localizedDate)) { + case DateKind.UTC: + utcOffsetText = "+00:00"; + break; + case DateKind.Local: + utcOffsetText = dateOffsetToString(localizedDate.getTimezoneOffset() * -60000); + break; + case DateKind.Unspecified: + utcOffsetText = dateOffsetToString(toLocalTime(localizedDate).getTimezoneOffset() * -60000); + break; + } + + const sign = utcOffsetText[0] === "-" ? "-" : "+"; + const hours = parseInt(utcOffsetText.substring(1, 3), 10); + const minutes = parseInt(utcOffsetText.substring(4, 6), 10); + + switch (tokenLength) { + case 1: + result += `${sign}${hours}`; + break; + case 2: + result += `${sign}${padWithZeros(hours, 2)}`; + break; + default: + result += `${sign}${padWithZeros(hours, 2)}:${padWithZeros(minutes, 2)}`; + break; + } + break; + case ":": + result += ":"; + cursorPos++; + break; + case "/": + result += "/"; + cursorPos++; + break; + case "'": + case '"': + const [quotedString, quotedStringLenght] = parseQuotedString(format, cursorPos); + result += quotedString; + cursorPos += quotedStringLenght; + break; + case "%": + const nextChar = parseNextChar(format, cursorPos); + if (nextChar >= 0 && nextChar !== "%".charCodeAt(0)) { + cursorPos += 2; + result += dateToStringWithCustomFormat(localizedDate, String.fromCharCode(nextChar), utc); + } else { + throw new Error("Invalid format string"); + } + break; + case "\\": + const nextChar2 = parseNextChar(format, cursorPos); + if (nextChar2 >= 0) { + cursorPos += 2; + result += String.fromCharCode(nextChar2); + } else { + throw new Error("Invalid format string"); + } + break; + default: + cursorPos++; + result += token; + break; + } + } + + return result; +} + export function kind(value: IDateTime): number { return value.kind || 0; } @@ -66,31 +420,6 @@ function dateToISOStringWithOffset(dateWithOffset: Date, offset: number) { return str.substring(0, str.length - 1) + dateOffsetToString(offset); } -function dateToStringWithCustomFormat(date: Date, format: string, utc: boolean) { - return format.replace(/(\w)\1*/g, (match: string) => { - let rep = Number.NaN; - switch (match.substring(0, 1)) { - case "y": - const y = utc ? date.getUTCFullYear() : date.getFullYear(); - rep = match.length < 4 ? y % 100 : y; break; - case "M": rep = (utc ? date.getUTCMonth() : date.getMonth()) + 1; break; - case "d": rep = utc ? date.getUTCDate() : date.getDate(); break; - case "H": rep = utc ? date.getUTCHours() : date.getHours(); break; - case "h": - const h = utc ? date.getUTCHours() : date.getHours(); - rep = h > 12 ? h % 12 : h; break; - case "m": rep = utc ? date.getUTCMinutes() : date.getMinutes(); break; - case "s": rep = utc ? date.getUTCSeconds() : date.getSeconds(); break; - case "f": rep = utc ? date.getUTCMilliseconds() : date.getMilliseconds(); break; - } - if (Number.isNaN(rep)) { - return match; - } else { - return padWithZeros(rep, match.length); - } - }); -} - function dateToStringWithOffset(date: IDateTimeOffset, format?: string) { const d = new Date(date.getTime() + (date.offset ?? 0)); if (typeof format !== "string") { diff --git a/src/fable-library-ts/Util.ts b/src/fable-library-ts/Util.ts index 04bcbda519..9bcb089558 100644 --- a/src/fable-library-ts/Util.ts +++ b/src/fable-library-ts/Util.ts @@ -270,22 +270,11 @@ export function lazyFromValue(v: T) { } export function padWithZeros(i: number, length: number) { - let str = i.toString(10); - while (str.length < length) { - str = "0" + str; - } - return str; + return i.toString(10).padStart(length, "0"); } export function padLeftAndRightWithZeros(i: number, lengthLeft: number, lengthRight: number) { - let str = i.toString(10); - while (str.length < lengthLeft) { - str = "0" + str; - } - while (str.length < lengthRight) { - str = str + "0"; - } - return str; + return i.toString(10).padStart(lengthLeft, "0").padEnd(lengthRight, "0"); } export function dateOffset(date: IDateTime | IDateTimeOffset): number { diff --git a/src/quicktest/tsconfig.json b/src/quicktest/tsconfig.json index d913ed8487..26fdd2d8a5 100644 --- a/src/quicktest/tsconfig.json +++ b/src/quicktest/tsconfig.json @@ -12,6 +12,7 @@ "noEmitOnError": true, "newLine": "lf", "forceConsistentCasingInFileNames": true, - "preserveWatchOutput": true + "preserveWatchOutput": true, + "moduleResolution": "Node" } } diff --git a/tests/Js/Main/DateTimeTests.fs b/tests/Js/Main/DateTimeTests.fs index b2d2a22907..e9c2727d79 100644 --- a/tests/Js/Main/DateTimeTests.fs +++ b/tests/Js/Main/DateTimeTests.fs @@ -21,8 +21,354 @@ let thatYearMilliseconds (dt: DateTime) = let tests = testList "DateTime" [ testCase "DateTime.ToString with custom format works" <| fun () -> - DateTime(2014, 9, 11, 16, 37, 0).ToString("HH:mm", CultureInfo.InvariantCulture) - |> equal "16:37" + DateTime(2014, 7, 1, 16, 37, 1, 2).ToString("r d", CultureInfo.InvariantCulture) + |> equal "r 1" + DateTime(2014, 7, 13, 16, 37, 1, 2).ToString("r d", CultureInfo.InvariantCulture) + |> equal "r 13" + + DateTime(2014, 7, 1, 16, 37, 0).ToString("r dd", CultureInfo.InvariantCulture) + |> equal "r 01" + DateTime(2014, 7, 21, 16, 37, 0).ToString("r dd", CultureInfo.InvariantCulture) + |> equal "r 21" + + DateTime(2014, 7, 7, 16, 37, 0).ToString("r ddd", CultureInfo.InvariantCulture) + |> equal "r Mon" + DateTime(2014, 7, 8, 16, 37, 0).ToString("r ddd", CultureInfo.InvariantCulture) + |> equal "r Tue" + DateTime(2014, 7, 9, 16, 37, 0).ToString("r ddd", CultureInfo.InvariantCulture) + |> equal "r Wed" + DateTime(2014, 7, 10, 16, 37, 0).ToString("r ddd", CultureInfo.InvariantCulture) + |> equal "r Thu" + DateTime(2014, 7, 11, 16, 37, 0).ToString("r ddd", CultureInfo.InvariantCulture) + |> equal "r Fri" + DateTime(2014, 7, 12, 16, 37, 0).ToString("r ddd", CultureInfo.InvariantCulture) + |> equal "r Sat" + DateTime(2014, 7, 13, 16, 37, 0).ToString("r ddd", CultureInfo.InvariantCulture) + |> equal "r Sun" + + DateTime(2014, 7, 7, 16, 37, 0).ToString("r dddd", CultureInfo.InvariantCulture) + |> equal "r Monday" + DateTime(2014, 7, 8, 16, 37, 0).ToString("r dddd", CultureInfo.InvariantCulture) + |> equal "r Tuesday" + DateTime(2014, 7, 9, 16, 37, 0).ToString("r dddd", CultureInfo.InvariantCulture) + |> equal "r Wednesday" + DateTime(2014, 7, 10, 16, 37, 0).ToString("r dddd", CultureInfo.InvariantCulture) + |> equal "r Thursday" + DateTime(2014, 7, 11, 16, 37, 0).ToString("r dddd", CultureInfo.InvariantCulture) + |> equal "r Friday" + DateTime(2014, 7, 12, 16, 37, 0).ToString("r dddd", CultureInfo.InvariantCulture) + |> equal "r Saturday" + DateTime(2014, 7, 13, 16, 37, 0).ToString("r dddd", CultureInfo.InvariantCulture) + |> equal "r Sunday" + + DateTime.Parse("2009-06-15T13:45:30.6175425").ToString("r f", CultureInfo.InvariantCulture) + |> equal "r 6" + DateTime.Parse("2009-06-15T13:45:30.05").ToString("r f", CultureInfo.InvariantCulture) + |> equal "r 0" + DateTime.Parse("2009-06-15T13:45:30.6175425").ToString("r ff", CultureInfo.InvariantCulture) + |> equal "r 61" + DateTime.Parse("2009-06-15T13:45:30.0050000").ToString("r ff", CultureInfo.InvariantCulture) + |> equal "r 00" + DateTime.Parse("2009-06-15T13:45:30.6175425").ToString("r fff", CultureInfo.InvariantCulture) + |> equal "r 617" + DateTime.Parse("2009-06-15T13:45:30.0005000").ToString("r fff", CultureInfo.InvariantCulture) + |> equal "r 000" + // JavaScript Date only support precision to the millisecond so we fill with 0 + DateTime.Parse("2009-06-15T13:45:30.617").ToString("r ffff", CultureInfo.InvariantCulture) + |> equal "r 6170" + DateTime.Parse("2009-06-15T13:45:30.000").ToString("r ffff", CultureInfo.InvariantCulture) + |> equal "r 0000" + DateTime.Parse("2009-06-15T13:45:30.617").ToString("r fffff", CultureInfo.InvariantCulture) + |> equal "r 61700" + DateTime.Parse("2009-06-15T13:45:30.000").ToString("r fffff", CultureInfo.InvariantCulture) + |> equal "r 00000" + DateTime.Parse("2009-06-15T13:45:30.617").ToString("r ffffff", CultureInfo.InvariantCulture) + |> equal "r 617000" + DateTime.Parse("2009-06-15T13:45:30.000").ToString("r ffffff", CultureInfo.InvariantCulture) + |> equal "r 000000" + DateTime.Parse("2009-06-15T13:45:30.617").ToString("r fffffff", CultureInfo.InvariantCulture) + |> equal "r 6170000" + + DateTime.Parse("2009-06-15T13:45:30.6175425").ToString("r F", CultureInfo.InvariantCulture) + |> equal "r 6" + DateTime.Parse("2009-06-15T13:45:30.05").ToString("r F", CultureInfo.InvariantCulture) + |> equal "r " + DateTime.Parse("2009-06-15T13:45:30.6175425").ToString("r FF", CultureInfo.InvariantCulture) + |> equal "r 61" + DateTime.Parse("2009-06-15T13:45:30.0050000").ToString("r FF", CultureInfo.InvariantCulture) + |> equal "r " + DateTime.Parse("2009-06-15T13:45:30.6175425").ToString("r FFF", CultureInfo.InvariantCulture) + |> equal "r 617" + DateTime.Parse("2009-06-15T13:45:30.0005000").ToString("r FFF", CultureInfo.InvariantCulture) + |> equal "r " + // JavaScript Date only support precision to the millisecond so we fill with 0 + DateTime.Parse("2009-06-15T13:45:30.617").ToString("r FFFF", CultureInfo.InvariantCulture) + |> equal "r 617" + DateTime.Parse("2009-06-15T13:45:30.000").ToString("r FFFF", CultureInfo.InvariantCulture) + |> equal "r " + DateTime.Parse("2009-06-15T13:45:30.617").ToString("r FFFFF", CultureInfo.InvariantCulture) + |> equal "r 617" + DateTime.Parse("2009-06-15T13:45:30.000").ToString("r FFFFF", CultureInfo.InvariantCulture) + |> equal "r " + DateTime.Parse("2009-06-15T13:45:30.061").ToString("r FFFFFF", CultureInfo.InvariantCulture) + |> equal "r 061" + DateTime.Parse("2009-06-15T13:45:30.000").ToString("r FFFFFF", CultureInfo.InvariantCulture) + |> equal "r " + DateTime.Parse("2009-06-15T13:45:30.617").ToString("r FFFFFFF", CultureInfo.InvariantCulture) + |> equal "r 617" + DateTime.Parse("2009-06-15T13:45:30.000").ToString("r FFFFFFF", CultureInfo.InvariantCulture) + |> equal "r " + + DateTime(2014, 7, 1, 16, 37, 0).ToString("r g", CultureInfo.InvariantCulture) + |> equal "r A.D." + + DateTime(2014, 7, 1, 16, 37, 0).ToString("r gg", CultureInfo.InvariantCulture) + |> equal "r A.D." + + DateTime(2014, 7, 1, 16, 37, 0).ToString("r h", CultureInfo.InvariantCulture) + |> equal "r 4" + DateTime(2014, 7, 1, 4, 37, 0).ToString("r h", CultureInfo.InvariantCulture) + |> equal "r 4" + + DateTime(2014, 7, 1, 16, 37, 0).ToString("r hh", CultureInfo.InvariantCulture) + |> equal "r 04" + DateTime(2014, 7, 1, 4, 37, 0).ToString("r hh", CultureInfo.InvariantCulture) + |> equal "r 04" + + DateTime(2014, 7, 1, 4, 37, 0).ToString("r H", CultureInfo.InvariantCulture) + |> equal "r 4" + DateTime(2014, 7, 1, 16, 37, 0).ToString("r H", CultureInfo.InvariantCulture) + |> equal "r 16" + + DateTime(2014, 7, 1, 4, 37, 0).ToString("r HH", CultureInfo.InvariantCulture) + |> equal "r 04" + DateTime(2014, 7, 1, 16, 37, 0).ToString("r HH", CultureInfo.InvariantCulture) + |> equal "r 16" + + DateTime(2014, 7, 1, 16, 37, 0).ToString("r K", CultureInfo.InvariantCulture) + |> equal "r " + DateTime(2014, 7, 1, 16, 37, 0, DateTimeKind.Utc).ToString("r K", CultureInfo.InvariantCulture) + |> equal "r Z" + + // // Timezone dependent (test is configured for Europe/Paris timezone) + // DateTime(2014, 7, 1, 16, 37, 0, DateTimeKind.Local).ToString("r K", CultureInfo.InvariantCulture) + // |> equal "r +02:00" + + DateTime(2014, 7, 1, 16, 37, 0, DateTimeKind.Unspecified).ToString("r K", CultureInfo.InvariantCulture) + |> equal "r " + + DateTime(2014, 7, 1, 16, 3, 0).ToString("r m", CultureInfo.InvariantCulture) + |> equal "r 3" + DateTime(2014, 7, 1, 16, 37, 0).ToString("r m", CultureInfo.InvariantCulture) + |> equal "r 37" + + DateTime(2014, 7, 1, 16, 3, 0).ToString("r mm", CultureInfo.InvariantCulture) + |> equal "r 03" + DateTime(2014, 7, 1, 16, 37, 0).ToString("r mm", CultureInfo.InvariantCulture) + |> equal "r 37" + + DateTime(2014, 7, 1, 16, 37, 0).ToString("r M", CultureInfo.InvariantCulture) + |> equal "r 7" + DateTime(2014, 11, 1, 16, 37, 0).ToString("r M", CultureInfo.InvariantCulture) + |> equal "r 11" + + DateTime(2014, 7, 1, 16, 37, 0).ToString("r MM", CultureInfo.InvariantCulture) + |> equal "r 07" + DateTime(2014, 11, 1, 16, 37, 0).ToString("r MM", CultureInfo.InvariantCulture) + |> equal "r 11" + + DateTime(2014, 1, 1, 16, 37, 0).ToString("r MMM", CultureInfo.InvariantCulture) + |> equal "r Jan" + DateTime(2014, 2, 1, 16, 37, 0).ToString("r MMM", CultureInfo.InvariantCulture) + |> equal "r Feb" + DateTime(2014, 3, 1, 16, 37, 0).ToString("r MMM", CultureInfo.InvariantCulture) + |> equal "r Mar" + DateTime(2014, 4, 1, 16, 37, 0).ToString("r MMM", CultureInfo.InvariantCulture) + |> equal "r Apr" + DateTime(2014, 5, 1, 16, 37, 0).ToString("r MMM", CultureInfo.InvariantCulture) + |> equal "r May" + DateTime(2014, 6, 1, 16, 37, 0).ToString("r MMM", CultureInfo.InvariantCulture) + |> equal "r Jun" + DateTime(2014, 7, 1, 16, 37, 0).ToString("r MMM", CultureInfo.InvariantCulture) + |> equal "r Jul" + DateTime(2014, 8, 1, 16, 37, 0).ToString("r MMM", CultureInfo.InvariantCulture) + |> equal "r Aug" + DateTime(2014, 9, 1, 16, 37, 0).ToString("r MMM", CultureInfo.InvariantCulture) + |> equal "r Sep" + DateTime(2014, 10, 1, 16, 37, 0).ToString("r MMM", CultureInfo.InvariantCulture) + |> equal "r Oct" + DateTime(2014, 11, 1, 16, 37, 0).ToString("r MMM", CultureInfo.InvariantCulture) + |> equal "r Nov" + DateTime(2014, 12, 1, 16, 37, 0).ToString("r MMM", CultureInfo.InvariantCulture) + |> equal "r Dec" + + DateTime(2014, 1, 1, 16, 37, 0).ToString("r MMMM", CultureInfo.InvariantCulture) + |> equal "r January" + DateTime(2014, 2, 1, 16, 37, 0).ToString("r MMMM", CultureInfo.InvariantCulture) + |> equal "r February" + DateTime(2014, 3, 1, 16, 37, 0).ToString("r MMMM", CultureInfo.InvariantCulture) + |> equal "r March" + DateTime(2014, 4, 1, 16, 37, 0).ToString("r MMMM", CultureInfo.InvariantCulture) + |> equal "r April" + DateTime(2014, 5, 1, 16, 37, 0).ToString("r MMMM", CultureInfo.InvariantCulture) + |> equal "r May" + DateTime(2014, 6, 1, 16, 37, 0).ToString("r MMMM", CultureInfo.InvariantCulture) + |> equal "r June" + DateTime(2014, 7, 1, 16, 37, 0).ToString("r MMMM", CultureInfo.InvariantCulture) + |> equal "r July" + DateTime(2014, 8, 1, 16, 37, 0).ToString("r MMMM", CultureInfo.InvariantCulture) + |> equal "r August" + DateTime(2014, 9, 1, 16, 37, 0).ToString("r MMMM", CultureInfo.InvariantCulture) + |> equal "r September" + DateTime(2014, 10, 1, 16, 37, 0).ToString("r MMMM", CultureInfo.InvariantCulture) + |> equal "r October" + DateTime(2014, 11, 1, 16, 37, 0).ToString("r MMMM", CultureInfo.InvariantCulture) + |> equal "r November" + DateTime(2014, 12, 1, 16, 37, 0).ToString("r MMMM", CultureInfo.InvariantCulture) + |> equal "r December" + + DateTime(2014, 7, 1, 16, 37, 3).ToString("r s", CultureInfo.InvariantCulture) + |> equal "r 3" + DateTime(2014, 7, 1, 16, 37, 31).ToString("r s", CultureInfo.InvariantCulture) + |> equal "r 31" + + DateTime(2014, 7, 1, 16, 37, 3).ToString("r ss", CultureInfo.InvariantCulture) + |> equal "r 03" + DateTime(2014, 7, 1, 16, 37, 31).ToString("r ss", CultureInfo.InvariantCulture) + |> equal "r 31" + + DateTime(2014, 7, 1, 1, 37, 0).ToString("r t", CultureInfo.InvariantCulture) + |> equal "r A" + DateTime(2014, 7, 1, 16, 37, 0).ToString("r t", CultureInfo.InvariantCulture) + |> equal "r P" + DateTime(2014, 7, 1, 1, 37, 0).ToString("r tt", CultureInfo.InvariantCulture) + |> equal "r AM" + DateTime(2014, 7, 1, 16, 37, 0).ToString("r tt", CultureInfo.InvariantCulture) + |> equal "r PM" + + DateTime(1,1,1).ToString("r y", CultureInfo.InvariantCulture) + |> equal "r 1" + DateTime(0900,1,1).ToString("r y", CultureInfo.InvariantCulture) + |> equal "r 0" + DateTime(1900,1,1).ToString("r y", CultureInfo.InvariantCulture) + |> equal "r 0" + DateTime(2009,1,1).ToString("r y", CultureInfo.InvariantCulture) + |> equal "r 9" + DateTime(2019,1,1).ToString("r y", CultureInfo.InvariantCulture) + |> equal "r 19" + + DateTime(1,1,1).ToString("r yy", CultureInfo.InvariantCulture) + |> equal "r 01" + DateTime(0900,1,1).ToString("r yy", CultureInfo.InvariantCulture) + |> equal "r 00" + DateTime(1900,1,1).ToString("r yy", CultureInfo.InvariantCulture) + |> equal "r 00" + DateTime(2009,1,1).ToString("r yy", CultureInfo.InvariantCulture) + |> equal "r 09" + DateTime(2019,1,1).ToString("r yy", CultureInfo.InvariantCulture) + |> equal "r 19" + + DateTime(1,1,1).ToString("r yyy", CultureInfo.InvariantCulture) + |> equal "r 001" + DateTime(0900,1,1).ToString("r yyy", CultureInfo.InvariantCulture) + |> equal "r 900" + DateTime(1900,1,1).ToString("r yyy", CultureInfo.InvariantCulture) + |> equal "r 1900" + DateTime(2009,1,1).ToString("r yyy", CultureInfo.InvariantCulture) + |> equal "r 2009" + DateTime(2019,1,1).ToString("r yyy", CultureInfo.InvariantCulture) + |> equal "r 2019" + + DateTime(1,1,1).ToString("r yyyy", CultureInfo.InvariantCulture) + |> equal "r 0001" + DateTime(0900,1,1).ToString("r yyyy", CultureInfo.InvariantCulture) + |> equal "r 0900" + DateTime(1900,1,1).ToString("r yyyy", CultureInfo.InvariantCulture) + |> equal "r 1900" + DateTime(2009,1,1).ToString("r yyyy", CultureInfo.InvariantCulture) + |> equal "r 2009" + DateTime(2019,1,1).ToString("r yyyy", CultureInfo.InvariantCulture) + |> equal "r 2019" + + + DateTime(1,1,1).ToString("r yyyyy", CultureInfo.InvariantCulture) + |> equal "r 00001" + DateTime(0900,1,1).ToString("r yyyyy", CultureInfo.InvariantCulture) + |> equal "r 00900" + DateTime(1900,1,1).ToString("r yyyyy", CultureInfo.InvariantCulture) + |> equal "r 01900" + DateTime(2009,1,1).ToString("r yyyyy", CultureInfo.InvariantCulture) + |> equal "r 02009" + DateTime(2019,1,1).ToString("r yyyyy", CultureInfo.InvariantCulture) + |> equal "r 02019" + + + DateTime(2014, 7, 1, 16, 37, 0, DateTimeKind.Utc).ToString("r z", CultureInfo.InvariantCulture) + |> equal "r +0" + + // Timezone dependent (test is configured for Europe/Paris timezone) + // DateTime(2014, 7, 1, 16, 37, 0).ToString("r z", CultureInfo.InvariantCulture) + // |> equal "r +2" + // DateTime(2014, 7, 1, 16, 37, 0, DateTimeKind.Local).ToString("r z", CultureInfo.InvariantCulture) + // |> equal "r +2" + + + DateTime(2014, 7, 1, 16, 37, 0, DateTimeKind.Utc).ToString("r zz", CultureInfo.InvariantCulture) + |> equal "r +00" + // Timezone dependent (test is configured for Europe/Paris timezone) + // DateTime(2014, 7, 1, 16, 37, 0).ToString("r zz", CultureInfo.InvariantCulture) + // |> equal "r +02" + // DateTime(2014, 7, 1, 16, 37, 0, DateTimeKind.Local).ToString("r zz", CultureInfo.InvariantCulture) + // |> equal "r +02" + + DateTime(2014, 7, 1, 16, 37, 0, DateTimeKind.Utc).ToString("r zzz", CultureInfo.InvariantCulture) + |> equal "r +00:00" + // Timezone dependent (test is configured for Europe/Paris timezone) + // DateTime(2014, 7, 1, 16, 37, 0).ToString("r zzz", CultureInfo.InvariantCulture) + // |> equal "r +02:00" + // DateTime(2014, 7, 1, 16, 37, 0, DateTimeKind.Local).ToString("r zzz", CultureInfo.InvariantCulture) + // |> equal "r +02:00" + + // Time separator + DateTime(2014, 7, 1, 16, 37, 0).ToString("r :", CultureInfo.InvariantCulture) + |> equal "r :" + + // Date separator + DateTime(2014, 7, 1, 16, 37, 0).ToString("r /", CultureInfo.InvariantCulture) + |> equal "r /" + + // String quotation + DateTime(2014, 7, 1, 16, 37, 0).ToString("r \"hh\" h", CultureInfo.InvariantCulture) + |> equal "r hh 4" + DateTime(2014, 7, 1, 16, 37, 0).ToString("r 'hh' h", CultureInfo.InvariantCulture) + |> equal "r hh 4" + DateTime(2014, 7, 1, 16, 37, 0).ToString("r \'hh\'", CultureInfo.InvariantCulture) + |> equal "r hh" + + // Format character + DateTime(2014, 7, 1, 16, 37, 0).ToString("r %h", CultureInfo.InvariantCulture) + |> equal "r 4" + DateTime(2014, 7, 1, 16, 37, 0).ToString("r %hh", CultureInfo.InvariantCulture) + |> equal "r 44" + + // Escape character + DateTime(2014, 7, 1, 16, 37, 0, DateTimeKind.Utc).ToString("r \zz", CultureInfo.InvariantCulture) + |> equal "r z+0" + DateTime(2014, 7, 1, 16, 37, 0, DateTimeKind.Utc).ToString("r \\zz", CultureInfo.InvariantCulture) + |> equal "r z+0" + DateTime(2014, 7, 1, 16, 37, 0, DateTimeKind.Utc).ToString("r \\\zz", CultureInfo.InvariantCulture) + |> equal "r \+00" + DateTime(2014, 7, 1, 16, 37, 0, DateTimeKind.Utc).ToString("r \\z\\z", CultureInfo.InvariantCulture) + |> equal "r zz" + + // Escape character with verbatim string + DateTime(2014, 7, 1, 16, 37, 0, DateTimeKind.Utc).ToString("""r \zz""", CultureInfo.InvariantCulture) + |> equal "r z+0" + DateTime(2014, 7, 1, 16, 37, 0, DateTimeKind.Utc).ToString("""r \\zz""", CultureInfo.InvariantCulture) + |> equal "r \+00" + DateTime(2014, 7, 1, 16, 37, 0, DateTimeKind.Utc).ToString("""r \\\zz""", CultureInfo.InvariantCulture) + |> equal "r \z+0" + testCase "DateTime.ToString without separator works" <| fun () -> // See #1131 DateTime(2017, 9, 5).ToString("yyyyMM")