Skip to content
Merged
117 changes: 104 additions & 13 deletions src/fable-library-py/fable_library/date.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,48 @@ def to_long_time_string(date: datetime) -> str:
return datetime.strftime(date, "%H:%M:%S")


def _to_plain_utc(date: datetime) -> datetime:
# Strip any datetime subclass (e.g. DateTimeOffset) so astimezone's internal
# reconstruction doesn't hit an incompatible __new__ signature.
plain = datetime(
date.year, date.month, date.day, date.hour, date.minute, date.second, date.microsecond, tzinfo=date.tzinfo
)
return plain.astimezone(UTC)


def to_rfc1123_string(date: datetime) -> str:
"""RFC 1123: "Thu, 01 Jan 2009 00:00:00 GMT" — always UTC"""
utc_date = _to_plain_utc(date)
return (
short_days[day_of_week(utc_date)]
+ ", "
+ utc_date.strftime("%d ")
+ short_months[utc_date.month - 1]
+ utc_date.strftime(" %Y %H:%M:%S GMT")
)


def to_sortable_string(date: datetime) -> str:
"""Sortable ISO 8601, no timezone: "2009-06-15T13:45:30" """
return date.strftime("%Y-%m-%dT%H:%M:%S")


def to_universal_sortable_string(date: datetime) -> str:
"""Universal sortable: "2009-06-15 13:45:30Z" — always UTC"""
utc_date = _to_plain_utc(date)
return utc_date.strftime("%Y-%m-%d %H:%M:%SZ")


def to_month_day_string(date: datetime) -> str:
"""Month/day (InvariantCulture "MMMM dd"): "June 15" """
return long_months[date.month - 1] + " " + f"{date.day:02d}"


def to_year_month_string(date: datetime) -> str:
"""Year/month (InvariantCulture "yyyy MMMM"): "2009 June" """
return str(date.year) + " " + long_months[date.month - 1]


def parse_repeat_token(format: str, pos: int, pattern_char: str) -> int:
token_length = 0
internal_pos = pos
Expand Down Expand Up @@ -426,6 +468,35 @@ def date_to_string_with_offset(date: datetime, format: str | None = None) -> str
return date.strftime("%Y-%m-%dT%H:%M:%S.%f%z")
case "O" | "o":
return date.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
case "D":
return to_long_date_string(date)
case "d":
return to_short_date_string(date)
case "F":
return to_long_date_string(date) + " " + to_long_time_string(date)
case "f":
return to_long_date_string(date) + " " + to_short_time_string(date)
case "G":
return to_short_date_string(date) + " " + to_long_time_string(date)
case "g":
return to_short_date_string(date) + " " + to_short_time_string(date)
case "M" | "m":
return to_month_day_string(date)
case "R" | "r":
return to_rfc1123_string(date)
case "s":
return to_sortable_string(date)
case "T":
return to_long_time_string(date)
case "t":
return to_short_time_string(date)
case "u":
return to_universal_sortable_string(date)
case "U":
utc_date = _to_plain_utc(date)
return to_long_date_string(utc_date) + " " + to_long_time_string(utc_date)
case "Y" | "y":
return to_year_month_string(date)
case _ if len(format) == 1:
raise Exception("Unrecognized Date print format")
case _:
Expand All @@ -435,24 +506,44 @@ def date_to_string_with_offset(date: datetime, format: str | None = None) -> str
def date_to_string_with_kind(date: datetime, format: str | None = None) -> str:
utc = date.tzinfo == UTC

if not format:
return date.isoformat() if utc else str(date)
elif len(format) == 1:
if format == "d":
match format:
case None | "":
return date.isoformat() if utc else str(date)
case "d":
return to_short_date_string(date)
elif format == "D":
case "D":
return to_long_date_string(date)
elif format == "T":
case "F":
return to_long_date_string(date) + " " + to_long_time_string(date)
case "f":
return to_long_date_string(date) + " " + to_short_time_string(date)
case "G":
return to_short_date_string(date) + " " + to_long_time_string(date)
case "g":
return to_short_date_string(date) + " " + to_short_time_string(date)
case "M" | "m":
return to_month_day_string(date)
case "O" | "o":
return date.astimezone().isoformat(timespec="milliseconds")
case "R" | "r":
return to_rfc1123_string(date)
case "s":
return to_sortable_string(date)
case "T":
return to_long_time_string(date)
elif format == "t":
case "t":
return to_short_time_string(date)
elif format == "O" or format == "o":
return date.astimezone().isoformat(timespec="milliseconds")
else:
case "u":
return to_universal_sortable_string(date)
case "U":
utc_date = _to_plain_utc(date)
return to_long_date_string(utc_date) + " " + to_long_time_string(utc_date)
case "Y" | "y":
return to_year_month_string(date)
case _ if len(format) == 1:
raise Exception("Unrecognized Date print format")

else:
return date_to_string_with_custom_format(date, format, utc)
case _:
return date_to_string_with_custom_format(date, format, utc)


def to_string(date: datetime, format: str | None = None, provider: Any | None = None) -> str:
Expand Down
77 changes: 74 additions & 3 deletions src/fable-library-ts/Date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,9 +410,28 @@ function dateToStringWithOffset(date: IDateTimeOffset, format?: string) {
switch (format) {
case "D": return dateToString_D(d);
case "d": return dateToString_d(d);
case "F": return dateToString_D(d) + " " + dateToString_T(d);
case "f": return dateToString_D(d) + " " + dateToString_t(d);
case "G": return dateToString_d(d) + " " + dateToString_T(d);
case "g": return dateToString_d(d) + " " + dateToString_t(d);
case "M": case "m": return dateToString_M(d);
case "O": case "o": return dateToISOStringWithOffset(d, (date.offset ?? 0));
case "R": case "r": {
const utcDate = DateTime(date.getTime(), DateTimeKind.Utc);
return dateToString_R(utcDate);
}
case "s": return dateToString_s(d);
case "T": return dateToString_T(toUniversalTime(d));
case "t": return dateToString_t(toUniversalTime(d));
case "O": case "o": return dateToISOStringWithOffset(d, (date.offset ?? 0));
case "u": {
const utcDate = DateTime(date.getTime(), DateTimeKind.Utc);
return dateToString_u(utcDate);
}
case "U": {
const utcDate = DateTime(date.getTime(), DateTimeKind.Utc);
return dateToString_D(utcDate) + " " + dateToString_T(utcDate);
}
case "Y": case "y": return dateToString_Y(d);
default: throw new Exception("Unrecognized Date print format");
}
} else {
Expand Down Expand Up @@ -444,6 +463,49 @@ function dateToString_t(date: IDateTime) {
+ ":" + padWithZeros(minute(date), 2);
}

// RFC 1123: "Thu, 01 Jan 2009 00:00:00 GMT" — always UTC
function dateToString_R(date: IDateTime) {
const utcDate = toUniversalTime(date);
return shortDays[dayOfWeek(utcDate)] + ", "
+ padWithZeros(day(utcDate), 2) + " "
+ shortMonths[month(utcDate) - 1] + " "
+ year(utcDate) + " "
+ padWithZeros(hour(utcDate), 2) + ":"
+ padWithZeros(minute(utcDate), 2) + ":"
+ padWithZeros(second(utcDate), 2) + " GMT";
}

// Sortable ISO 8601, no timezone: "2009-06-15T13:45:30"
function dateToString_s(date: IDateTime) {
return padWithZeros(year(date), 4) + "-"
+ padWithZeros(month(date), 2) + "-"
+ padWithZeros(day(date), 2) + "T"
+ padWithZeros(hour(date), 2) + ":"
+ padWithZeros(minute(date), 2) + ":"
+ padWithZeros(second(date), 2);
}

// Universal sortable: "2009-06-15 13:45:30Z" — always UTC
function dateToString_u(date: IDateTime) {
const utcDate = toUniversalTime(date);
return padWithZeros(year(utcDate), 4) + "-"
+ padWithZeros(month(utcDate), 2) + "-"
+ padWithZeros(day(utcDate), 2) + " "
+ padWithZeros(hour(utcDate), 2) + ":"
+ padWithZeros(minute(utcDate), 2) + ":"
+ padWithZeros(second(utcDate), 2) + "Z";
}

// Month/day (InvariantCulture "MMMM dd"): "June 15"
function dateToString_M(date: IDateTime) {
return longMonths[month(date) - 1] + " " + padWithZeros(day(date), 2);
}

// Year/month (InvariantCulture "yyyy MMMM"): "2009 June"
function dateToString_Y(date: IDateTime) {
return year(date) + " " + longMonths[month(date) - 1];
}

function dateToStringWithKind(date: IDateTime, format?: string) {
const utc = date.kind === DateTimeKind.Utc;
if (typeof format !== "string") {
Expand All @@ -452,10 +514,19 @@ function dateToStringWithKind(date: IDateTime, format?: string) {
switch (format) {
case "D": return dateToString_D(date);
case "d": return dateToString_d(date);
case "F": return dateToString_D(date) + " " + dateToString_T(date);
case "f": return dateToString_D(date) + " " + dateToString_t(date);
case "G": return dateToString_d(date) + " " + dateToString_T(date);
case "g": return dateToString_d(date) + " " + dateToString_t(date);
case "M": case "m": return dateToString_M(date);
case "O": case "o": return dateToISOString(date, utc);
case "R": case "r": return dateToString_R(date);
case "s": return dateToString_s(date);
case "T": return dateToString_T(date);
case "t": return dateToString_t(date);
case "O": case "o":
return dateToISOString(date, utc);
case "u": return dateToString_u(date);
case "U": return dateToString_D(toUniversalTime(date)) + " " + dateToString_T(toUniversalTime(date));
case "Y": case "y": return dateToString_Y(date);
default:
throw new Exception("Unrecognized Date print format");
}
Expand Down
15 changes: 15 additions & 0 deletions tests/Js/Main/DateTimeOffsetTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,21 @@ let tests =
|> format
|> equal "05:07"

testCase "DateTimeOffset.ToString('R') works" <| fun _ ->
// R always formats in UTC
DateTimeOffset(2014, 9, 1, 16, 37, 2, TimeSpan.FromHours 2).ToString("R", CultureInfo.InvariantCulture)
|> equal "Mon, 01 Sep 2014 14:37:02 GMT"

testCase "DateTimeOffset.ToString('u') works" <| fun _ ->
// u always formats in UTC
DateTimeOffset(2014, 9, 1, 16, 37, 2, TimeSpan.FromHours 2).ToString("u", CultureInfo.InvariantCulture)
|> equal "2014-09-01 14:37:02Z"

testCase "DateTimeOffset.ToString('s') works" <| fun _ ->
// s uses the offset-local time with no timezone designator
DateTimeOffset(2014, 9, 1, 16, 37, 2, TimeSpan.Zero).ToString("s", CultureInfo.InvariantCulture)
|> equal "2014-09-01T16:37:02"

testCase "DateTimeOffset from Year 1 to 99 works" <| fun () ->
let date = DateTimeOffset(1, 1, 2, 0, 0, 0, TimeSpan.Zero)
date.Year |> equal 1
Expand Down
54 changes: 54 additions & 0 deletions tests/Js/Main/DateTimeTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,60 @@ let tests =
// DateTime(2014, 9, 11, 16, 37, 2, DateTimeKind.Local).ToString("O")
// |> equal "2014-09-11T16:37:02.000+02:00" // Here the time zone is Europe/Paris (GMT+2)

testCase "DateTime.ToString('R') works" <| fun _ ->
let format (d: DateTime) = d.ToString("R", CultureInfo.InvariantCulture)
DateTime(2014, 9, 1, 16, 37, 2, DateTimeKind.Utc)
|> format
|> equal "Mon, 01 Sep 2014 16:37:02 GMT"

testCase "DateTime.ToString('s') works" <| fun _ ->
let format (d: DateTime) = d.ToString("s", CultureInfo.InvariantCulture)
DateTime(2014, 9, 1, 16, 37, 2, DateTimeKind.Utc)
|> format
|> equal "2014-09-01T16:37:02"

testCase "DateTime.ToString('u') works" <| fun _ ->
let format (d: DateTime) = d.ToString("u", CultureInfo.InvariantCulture)
DateTime(2014, 9, 1, 16, 37, 2, DateTimeKind.Utc)
|> format
|> equal "2014-09-01 16:37:02Z"

testCase "DateTime.ToString('F') works" <| fun _ ->
let format (d: DateTime) = d.ToString("F", CultureInfo.InvariantCulture)
DateTime(2014, 9, 1, 16, 37, 2, DateTimeKind.Utc)
|> format
|> equal "Monday, 01 September 2014 16:37:02"

testCase "DateTime.ToString('f') works" <| fun _ ->
let format (d: DateTime) = d.ToString("f", CultureInfo.InvariantCulture)
DateTime(2014, 9, 1, 16, 37, 2, DateTimeKind.Utc)
|> format
|> equal "Monday, 01 September 2014 16:37"

testCase "DateTime.ToString('G') works" <| fun _ ->
let format (d: DateTime) = d.ToString("G", CultureInfo.InvariantCulture)
DateTime(2014, 9, 1, 16, 37, 2, DateTimeKind.Utc)
|> format
|> equal "09/01/2014 16:37:02"

testCase "DateTime.ToString('g') works" <| fun _ ->
let format (d: DateTime) = d.ToString("g", CultureInfo.InvariantCulture)
DateTime(2014, 9, 1, 16, 37, 2, DateTimeKind.Utc)
|> format
|> equal "09/01/2014 16:37"

testCase "DateTime.ToString('M') works" <| fun _ ->
let format (d: DateTime) = d.ToString("M", CultureInfo.InvariantCulture)
DateTime(2014, 9, 1, 16, 37, 2, DateTimeKind.Utc)
|> format
|> equal "September 01"

testCase "DateTime.ToString('Y') works" <| fun _ ->
let format (d: DateTime) = d.ToString("Y", CultureInfo.InvariantCulture)
DateTime(2014, 9, 1, 16, 37, 2, DateTimeKind.Utc)
|> format
|> equal "2014 September"

testCase "DateTime from Year 1 to 99 works" <| fun () ->
let date = DateTime(1, 1, 2)
date.Year |> equal 1
Expand Down
45 changes: 45 additions & 0 deletions tests/Python/TestDateTime.fs
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,51 @@ let ``test DateTime.ToString with Round-trip format works for Utc`` () =
str.Replace("0000000Z", "000000Z")
|> equal "2014-09-11T16:37:02.000000Z"

[<Fact>]
let ``test DateTime.ToString("R") works`` () =
DateTime(2014, 9, 1, 16, 37, 2, DateTimeKind.Utc).ToString("R", CultureInfo.InvariantCulture)
|> equal "Mon, 01 Sep 2014 16:37:02 GMT"

[<Fact>]
let ``test DateTime.ToString("s") works`` () =
DateTime(2014, 9, 1, 16, 37, 2, DateTimeKind.Utc).ToString("s", CultureInfo.InvariantCulture)
|> equal "2014-09-01T16:37:02"

[<Fact>]
let ``test DateTime.ToString("u") works`` () =
DateTime(2014, 9, 1, 16, 37, 2, DateTimeKind.Utc).ToString("u", CultureInfo.InvariantCulture)
|> equal "2014-09-01 16:37:02Z"

[<Fact>]
let ``test DateTime.ToString full date long time format works`` () =
DateTime(2014, 9, 1, 16, 37, 2, DateTimeKind.Utc).ToString("F", CultureInfo.InvariantCulture)
|> equal "Monday, 01 September 2014 16:37:02"

[<Fact>]
let ``test DateTime.ToString full date short time format works`` () =
DateTime(2014, 9, 1, 16, 37, 2, DateTimeKind.Utc).ToString("f", CultureInfo.InvariantCulture)
|> equal "Monday, 01 September 2014 16:37"

[<Fact>]
let ``test DateTime.ToString general long time format works`` () =
DateTime(2014, 9, 1, 16, 37, 2, DateTimeKind.Utc).ToString("G", CultureInfo.InvariantCulture)
|> equal "09/01/2014 16:37:02"

[<Fact>]
let ``test DateTime.ToString general short time format works`` () =
DateTime(2014, 9, 1, 16, 37, 2, DateTimeKind.Utc).ToString("g", CultureInfo.InvariantCulture)
|> equal "09/01/2014 16:37"

[<Fact>]
let ``test DateTime.ToString("M") works`` () =
DateTime(2014, 9, 1, 16, 37, 2, DateTimeKind.Utc).ToString("M", CultureInfo.InvariantCulture)
|> equal "September 01"

[<Fact>]
let ``test DateTime.ToString("Y") works`` () =
DateTime(2014, 9, 1, 16, 37, 2, DateTimeKind.Utc).ToString("Y", CultureInfo.InvariantCulture)
|> equal "2014 September"

[<Fact>]
let ``test DateTime from Year 1 to 99 works`` () =
let date = DateTime(1, 1, 2)
Expand Down
Loading
Loading