diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index 91df8ecfe5..77d311c2be 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * [All] Add support for `Guid.CreateVersion7()` and `Guid.CreateVersion7(DateTimeOffset)` * [Rust] Add missing `System.Random` implementation and tests (by @ncave) * [Rust] Add missing `Array`, `List` and `Seq` module members and tests: `randomChoice`, `randomChoiceBy`, `randomChoiceWith`, `randomChoices`, `randomChoicesBy`, `randomChoicesWith`, `randomSample`, `randomSampleBy`, `randomSampleWith`, `randomShuffle`, `randomShuffleBy`, `randomShuffleWith` (by @ncave) +* [Beam] Implement missing DateTimeOffset members, add DateOnly and TimeOnly support ### Fixed diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index e68746bbc2..1b5216a1bf 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * [All] Add support for `Guid.CreateVersion7()` and `Guid.CreateVersion7(DateTimeOffset)` * [Rust] Add missing `System.Random` implementation and tests (by @ncave) * [Rust] Add missing `Array`, `List` and `Seq` module members and tests: `randomChoice`, `randomChoiceBy`, `randomChoiceWith`, `randomChoices`, `randomChoicesBy`, `randomChoicesWith`, `randomSample`, `randomSampleBy`, `randomSampleWith`, `randomShuffle`, `randomShuffleBy`, `randomShuffleWith` (by @ncave) +* [Beam] Implement missing DateTimeOffset members, add DateOnly and TimeOnly support ### Fixed diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index a44de13dbd..2da2630de4 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -2673,7 +2673,9 @@ let rec defaultof (_com: ICompiler) (_ctx: Context) (r: SourceLocation option) ( | Char -> CharConstant '\u0000' |> makeValue None | String -> Value(Null typ, r) | Builtin BclTimeSpan -> makeIntConst 0 + | Builtin BclTimeOnly -> makeIntConst 0 | Builtin BclDateTime -> Value(NewTuple([ makeIntConst 0; makeIntConst 0 ], false), r) + | Builtin BclDateOnly -> Helper.LibCall(_com, "fable_date_only", "min_value", typ, [], ?loc = r) | Builtin BclDateTimeOffset -> Value(NewTuple([ makeIntConst 0; makeIntConst 0; makeIntConst 0 ], false), r) | Builtin BclGuid -> makeStrConst "00000000-0000-0000-0000-000000000000" | _ -> Value(Null typ, r) @@ -3685,6 +3687,12 @@ let tryField (com: ICompiler) returnTyp ownerTyp fieldName : Expr option = | Types.datetime, "MinValue" -> Helper.LibCall(com, "fable_date", "min_value", returnTyp, [], ?loc = None) |> Some + | Types.datetimeOffset, ("MaxValue" | "MinValue" | "UnixEpoch") -> + Helper.LibCall(com, "fable_date_offset", Naming.lowerFirst fieldName, returnTyp, [], ?loc = None) + |> Some + | Types.dateOnly, ("MaxValue" | "MinValue") -> + Helper.LibCall(com, "fable_date_only", Naming.lowerFirst fieldName, returnTyp, [], ?loc = None) + |> Some | _ -> None | _ -> None @@ -4170,6 +4178,217 @@ let private dates | None -> None | _ -> None +/// Beam-specific System.DateOnly replacements. +/// DateOnly is represented as a DateTime tuple {Ticks, Kind} with time at midnight. +let private dateOnly + (com: ICompiler) + (_ctx: Context) + r + (t: Type) + (info: CallInfo) + (thisArg: Expr option) + (args: Expr list) + = + match info.CompiledName with + | ".ctor" when args.Length = 4 -> + "DateOnly constructor with the calendar parameter is not supported." + |> addError com _ctx.InlinePath r + + None + | ".ctor" -> Helper.LibCall(com, "fable_date_only", "create", t, args, ?loc = r) |> Some + | "ToString" -> + match thisArg with + | Some callee -> + match args with + | [] -> + Helper.LibCall(com, "fable_date_only", "to_string", t, [ callee ], ?loc = r) + |> Some + | [ fmt ] -> + Helper.LibCall(com, "fable_date_only", "to_string", t, [ callee; fmt ], ?loc = r) + |> Some + | [ fmt; _culture ] -> + Helper.LibCall(com, "fable_date_only", "to_string", t, [ callee; fmt ], ?loc = r) + |> Some + | _ -> None + | None -> None + | "AddDays" + | "AddMonths" + | "AddYears" -> + match thisArg with + | Some callee -> + let meth = Naming.removeGetSetPrefix info.CompiledName |> Naming.lowerFirst + + Helper.LibCall(com, "fable_date_only", meth, t, [ callee; args.Head ], ?loc = r) + |> Some + | None -> None + | "get_Year" -> + match thisArg with + | Some callee -> Helper.LibCall(com, "fable_date_only", "year", t, [ callee ], ?loc = r) |> Some + | None -> None + | "get_Month" -> + match thisArg with + | Some callee -> Helper.LibCall(com, "fable_date_only", "month", t, [ callee ], ?loc = r) |> Some + | None -> None + | "get_Day" -> + match thisArg with + | Some callee -> Helper.LibCall(com, "fable_date_only", "day", t, [ callee ], ?loc = r) |> Some + | None -> None + | "get_DayOfWeek" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_only", "day_of_week", t, [ callee ], ?loc = r) + |> Some + | None -> None + | "get_DayOfYear" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_only", "day_of_year", t, [ callee ], ?loc = r) + |> Some + | None -> None + | "get_DayNumber" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_only", "day_number", t, [ callee ], ?loc = r) + |> Some + | None -> None + | "get_MinValue" -> Helper.LibCall(com, "fable_date_only", "min_value", t, [], ?loc = r) |> Some + | "get_MaxValue" -> Helper.LibCall(com, "fable_date_only", "max_value", t, [], ?loc = r) |> Some + | "FromDayNumber" -> + Helper.LibCall(com, "fable_date_only", "from_day_number", t, args, ?loc = r) + |> Some + | "FromDateTime" -> + Helper.LibCall(com, "fable_date_only", "from_date_time", t, args, ?loc = r) + |> Some + | "ToDateTime" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_only", "to_date_time", t, callee :: args, ?loc = r) + |> Some + | None -> None + | "TryParse" -> Helper.LibCall(com, "fable_date_only", "try_parse", t, args, ?loc = r) |> Some + | "Parse" -> + let args = args |> List.take 1 + Helper.LibCall(com, "fable_date_only", "parse", t, args, ?loc = r) |> Some + | "Equals" -> + match thisArg with + | Some callee -> equals com r true callee args.Head |> Some + | None -> None + | "GetHashCode" -> + match thisArg with + | Some callee -> Helper.LibCall(com, "fable_comparison", "hash", t, [ callee ], ?loc = r) |> Some + | None -> None + | "CompareTo" -> + match thisArg with + | Some callee -> compare com r callee args.Head |> Some + | None -> None + | _ -> None + +/// Beam-specific System.TimeOnly replacements. +/// TimeOnly is represented as ticks since midnight (same unit as TimeSpan). +let private timeOnly + (com: ICompiler) + (_ctx: Context) + r + (t: Type) + (info: CallInfo) + (thisArg: Expr option) + (args: Expr list) + = + match info.CompiledName with + | ".ctor" -> + match args with + | [ ExprType(Number(Int64, _)) ] -> + Helper.LibCall(com, "fable_time_only", "from_ticks", t, args, ?loc = r) |> Some + | _ -> Helper.LibCall(com, "fable_time_only", "create", t, args, ?loc = r) |> Some + | "get_Hour" -> + match thisArg with + | Some callee -> Helper.LibCall(com, "fable_time_only", "hour", t, [ callee ], ?loc = r) |> Some + | None -> None + | "get_Minute" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_time_only", "minute", t, [ callee ], ?loc = r) + |> Some + | None -> None + | "get_Second" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_time_only", "second", t, [ callee ], ?loc = r) + |> Some + | None -> None + | "get_Millisecond" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_time_only", "millisecond", t, [ callee ], ?loc = r) + |> Some + | None -> None + | "get_Ticks" -> + match thisArg with + | Some callee -> Helper.LibCall(com, "fable_time_only", "ticks", t, [ callee ], ?loc = r) |> Some + | None -> None + | "get_MinValue" -> Helper.LibCall(com, "fable_time_only", "min_value", t, [], ?loc = r) |> Some + | "get_MaxValue" -> Helper.LibCall(com, "fable_time_only", "max_value", t, [], ?loc = r) |> Some + | "ToTimeSpan" -> thisArg + | "Add" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_time_only", "add", t, [ callee; args.Head ], ?loc = r) + |> Some + | None -> None + | "AddHours" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_time_only", "add_hours", t, [ callee; args.Head ], ?loc = r) + |> Some + | None -> None + | "AddMinutes" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_time_only", "add_minutes", t, [ callee; args.Head ], ?loc = r) + |> Some + | None -> None + | "IsBetween" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_time_only", "is_between", t, callee :: args, ?loc = r) + |> Some + | None -> None + | "ToString" -> + match thisArg with + | Some callee -> + match args with + | [] -> + Helper.LibCall(com, "fable_time_only", "to_string", t, [ callee ], ?loc = r) + |> Some + | [ fmt ] -> + Helper.LibCall(com, "fable_time_only", "to_string", t, [ callee; fmt ], ?loc = r) + |> Some + | [ fmt; _culture ] -> + Helper.LibCall(com, "fable_time_only", "to_string", t, [ callee; fmt ], ?loc = r) + |> Some + | _ -> None + | None -> None + | "TryParse" -> Helper.LibCall(com, "fable_time_only", "try_parse", t, args, ?loc = r) |> Some + | "Parse" -> + let args = args |> List.take 1 + Helper.LibCall(com, "fable_time_only", "parse", t, args, ?loc = r) |> Some + | "op_Subtraction" -> + Helper.LibCall(com, "fable_time_only", "op_subtraction", t, args, ?loc = r) + |> Some + | "Equals" -> + match thisArg with + | Some callee -> equals com r true callee args.Head |> Some + | None -> None + | "GetHashCode" -> + match thisArg with + | Some callee -> Helper.LibCall(com, "fable_comparison", "hash", t, [ callee ], ?loc = r) |> Some + | None -> None + | "CompareTo" -> + match thisArg with + | Some callee -> compare com r callee args.Head |> Some + | None -> None + | _ -> None + /// Beam-specific System.DateTimeOffset replacements. /// DateTimeOffset is represented as a 3-tuple {Ticks, Kind, OffsetTicks}. let private dateTimeOffsets @@ -4188,8 +4407,15 @@ let private dateTimeOffsets | ExprType(Number(Int64, _)) :: _ -> Helper.LibCall(com, "fable_date_offset", "from_ticks", t, args, ?loc = r) |> Some + | [ ExprType(DeclaredType(e, [])) ] when e.FullName = Types.datetime -> + // DateTimeOffset(DateTime) - single arg, default offset to 0 + Helper.LibCall(com, "fable_date_offset", "from_date", t, args @ [ makeIntConst 0 ], ?loc = r) + |> Some | ExprType(DeclaredType(e, [])) :: _ when e.FullName = Types.datetime -> Helper.LibCall(com, "fable_date_offset", "from_date", t, args, ?loc = r) |> Some + | ExprType(DeclaredType(e, [])) :: _ when e.FullName = Types.dateOnly -> + Helper.LibCall(com, "fable_date_offset", "from_date_time", t, args, ?loc = r) + |> Some | _ -> Helper.LibCall(com, "fable_date_offset", "create", t, args, ?loc = r) |> Some | "ToString" -> match thisArg with @@ -4221,13 +4447,13 @@ let private dateTimeOffsets | "get_LocalDateTime" -> match thisArg with | Some callee -> - Helper.LibCall(com, "fable_date_offset", "to_local_time", t, [ callee ], ?loc = r) + Helper.LibCall(com, "fable_date_offset", "local_date_time", t, [ callee ], ?loc = r) |> Some | None -> None | "get_UtcDateTime" -> match thisArg with | Some callee -> - Helper.LibCall(com, "fable_date_offset", "to_universal_time", t, [ callee ], ?loc = r) + Helper.LibCall(com, "fable_date_offset", "utc_date_time", t, [ callee ], ?loc = r) |> Some | None -> None | "get_Year" -> @@ -4270,24 +4496,185 @@ let private dateTimeOffsets Helper.LibCall(com, "fable_date_offset", "millisecond", t, [ callee ], ?loc = r) |> Some | None -> None + | "get_Microsecond" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "microsecond", t, [ callee ], ?loc = r) + |> Some + | None -> None | "get_Ticks" -> match thisArg with | Some callee -> Helper.LibCall(com, "fable_date_offset", "ticks", t, [ callee ], ?loc = r) |> Some | None -> None + | "get_UtcTicks" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "utc_ticks", t, [ callee ], ?loc = r) + |> Some + | None -> None + | "get_DayOfWeek" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "day_of_week", t, [ callee ], ?loc = r) + |> Some + | None -> None + | "get_DayOfYear" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "day_of_year", t, [ callee ], ?loc = r) + |> Some + | None -> None + | "get_Date" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "date", t, [ callee ], ?loc = r) + |> Some + | None -> None + | "get_TimeOfDay" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "time_of_day", t, [ callee ], ?loc = r) + |> Some + | None -> None + | "get_TotalOffsetMinutes" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "total_offset_minutes", t, [ callee ], ?loc = r) + |> Some + | None -> None | "get_Now" -> Helper.LibCall(com, "fable_date_offset", "now", t, [], ?loc = r) |> Some | "get_UtcNow" -> Helper.LibCall(com, "fable_date_offset", "utc_now", t, [], ?loc = r) |> Some | "get_MinValue" -> Helper.LibCall(com, "fable_date_offset", "min_value", t, [], ?loc = r) |> Some - | "TryParse" -> Helper.LibCall(com, "fable_date_offset", "try_parse", t, args, ?loc = r) |> Some + | "get_MaxValue" -> Helper.LibCall(com, "fable_date_offset", "max_value", t, [], ?loc = r) |> Some + | "get_UnixEpoch" -> Helper.LibCall(com, "fable_date_offset", "unix_epoch", t, [], ?loc = r) |> Some + | "ToLocalTime" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "to_local_time", t, [ callee ], ?loc = r) + |> Some + | None -> None + | "ToUniversalTime" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "to_universal_time", t, [ callee ], ?loc = r) + |> Some + | None -> None + | "ToOffset" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "to_offset", t, [ callee; args.Head ], ?loc = r) + |> Some + | None -> None + | "Add" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "add", t, [ callee; args.Head ], ?loc = r) + |> Some + | None -> None + | "Subtract" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "subtract", t, [ callee; args.Head ], ?loc = r) + |> Some + | None -> None + | "AddYears" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "add_years", t, [ callee; args.Head ], ?loc = r) + |> Some + | None -> None + | "AddMonths" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "add_months", t, [ callee; args.Head ], ?loc = r) + |> Some + | None -> None + | "AddDays" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "add_days", t, [ callee; args.Head ], ?loc = r) + |> Some + | None -> None + | "AddHours" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "add_hours", t, [ callee; args.Head ], ?loc = r) + |> Some + | None -> None + | "AddMinutes" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "add_minutes", t, [ callee; args.Head ], ?loc = r) + |> Some + | None -> None + | "AddSeconds" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "add_seconds", t, [ callee; args.Head ], ?loc = r) + |> Some + | None -> None + | "AddMilliseconds" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "add_milliseconds", t, [ callee; args.Head ], ?loc = r) + |> Some + | None -> None + | "AddTicks" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "add_ticks", t, [ callee; args.Head ], ?loc = r) + |> Some + | None -> None + | "op_Addition" -> + Helper.LibCall(com, "fable_date_offset", "op_addition", t, args, ?loc = r) + |> Some + | "op_Subtraction" -> + Helper.LibCall(com, "fable_date_offset", "op_subtraction", t, args, ?loc = r) + |> Some + | "Compare" -> Helper.LibCall(com, "fable_date_offset", "compare", t, args, ?loc = r) |> Some + | "CompareTo" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "compare", t, [ callee; args.Head ], ?loc = r) + |> Some + | None -> None | "Equals" -> match thisArg with - | Some callee -> equals com r true callee args.Head |> Some + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "equals", t, [ callee; args.Head ], ?loc = r) + |> Some + | None -> None + | "EqualsExact" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "equals_exact", t, [ callee; args.Head ], ?loc = r) + |> Some | None -> None | "GetHashCode" -> match thisArg with | Some callee -> Helper.LibCall(com, "fable_comparison", "hash", t, [ callee ], ?loc = r) |> Some | None -> None + | "FromUnixTimeSeconds" -> + Helper.LibCall(com, "fable_date_offset", "from_unix_time_seconds", t, args, ?loc = r) + |> Some + | "FromUnixTimeMilliseconds" -> + Helper.LibCall(com, "fable_date_offset", "from_unix_time_milliseconds", t, args, ?loc = r) + |> Some + | "ToUnixTimeSeconds" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "to_unix_time_seconds", t, [ callee ], ?loc = r) + |> Some + | None -> None + | "ToUnixTimeMilliseconds" -> + match thisArg with + | Some callee -> + Helper.LibCall(com, "fable_date_offset", "to_unix_time_milliseconds", t, [ callee ], ?loc = r) + |> Some + | None -> None + | "TryParse" -> Helper.LibCall(com, "fable_date_offset", "try_parse", t, args, ?loc = r) |> Some | _ -> None /// Beam-specific System.Uri replacements. @@ -4931,6 +5318,8 @@ let tryCall | Types.datetime -> dates com ctx r t info thisArg args | "System.Uri" -> uris com ctx r t info thisArg args | Types.datetimeOffset -> dateTimeOffsets com ctx r t info thisArg args + | Types.dateOnly -> dateOnly com ctx r t info thisArg args + | Types.timeOnly -> timeOnly com ctx r t info thisArg args | Types.timespan -> timeSpans com ctx r t info thisArg args | "System.Globalization.CultureInfo" -> match info.CompiledName with diff --git a/src/fable-library-beam/fable_date_offset.erl b/src/fable-library-beam/fable_date_offset.erl index b84dcae5e2..9a1f9a0443 100644 --- a/src/fable-library-beam/fable_date_offset.erl +++ b/src/fable-library-beam/fable_date_offset.erl @@ -3,6 +3,7 @@ create/7, create/8, from_date/2, from_ticks/2, + from_date_time/3, year/1, month/1, day/1, @@ -10,27 +11,60 @@ minute/1, second/1, millisecond/1, + microsecond/1, ticks/1, offset/1, date_time/1, + date/1, + time_of_day/1, + day_of_week/1, + day_of_year/1, + total_offset_minutes/1, + utc_ticks/1, now/0, utc_now/0, min_value/0, + max_value/0, + unix_epoch/0, + local_date_time/1, + utc_date_time/1, to_local_time/1, to_universal_time/1, + to_offset/2, + add/2, + subtract/2, + add_years/2, + add_months/2, + add_days/2, + add_hours/2, + add_minutes/2, + add_seconds/2, + add_milliseconds/2, + add_ticks/2, + op_addition/2, + op_subtraction/2, + compare/2, + equals/2, + equals_exact/2, + from_unix_time_seconds/1, + from_unix_time_milliseconds/1, + to_unix_time_seconds/1, + to_unix_time_milliseconds/1, try_parse/2, to_string/1, to_string/2, to_string/3 ]). -type datetimeoffset() :: {integer(), 0 | 1 | 2, integer()}. +-type datetime() :: {integer(), 0 | 1 | 2}. -spec create(integer(), integer(), integer(), integer(), integer(), integer(), integer()) -> datetimeoffset(). -spec create( integer(), integer(), integer(), integer(), integer(), integer(), integer(), integer() ) -> datetimeoffset(). --spec from_date({integer(), integer()}, integer()) -> datetimeoffset(). +-spec from_date(datetime(), integer()) -> datetimeoffset(). -spec from_ticks(integer(), integer()) -> datetimeoffset(). +-spec from_date_time(datetime(), integer(), integer()) -> datetimeoffset(). -spec year(datetimeoffset()) -> integer(). -spec month(datetimeoffset()) -> integer(). -spec day(datetimeoffset()) -> integer(). @@ -38,14 +72,45 @@ -spec minute(datetimeoffset()) -> integer(). -spec second(datetimeoffset()) -> integer(). -spec millisecond(datetimeoffset()) -> integer(). +-spec microsecond(datetimeoffset()) -> integer(). -spec ticks(datetimeoffset()) -> integer(). -spec offset(datetimeoffset()) -> integer(). --spec date_time(datetimeoffset()) -> {integer(), integer()}. +-spec date_time(datetimeoffset()) -> datetime(). +-spec date(datetimeoffset()) -> datetime(). +-spec time_of_day(datetimeoffset()) -> integer(). +-spec day_of_week(datetimeoffset()) -> integer(). +-spec day_of_year(datetimeoffset()) -> integer(). +-spec total_offset_minutes(datetimeoffset()) -> integer(). +-spec utc_ticks(datetimeoffset()) -> integer(). -spec now() -> datetimeoffset(). -spec utc_now() -> datetimeoffset(). -spec min_value() -> datetimeoffset(). +-spec max_value() -> datetimeoffset(). +-spec unix_epoch() -> datetimeoffset(). +-spec local_date_time(datetimeoffset()) -> datetime(). +-spec utc_date_time(datetimeoffset()) -> datetime(). -spec to_local_time(datetimeoffset()) -> datetimeoffset(). -spec to_universal_time(datetimeoffset()) -> datetimeoffset(). +-spec to_offset(datetimeoffset(), integer()) -> datetimeoffset(). +-spec add(datetimeoffset(), integer()) -> datetimeoffset(). +-spec subtract(datetimeoffset(), datetimeoffset() | integer()) -> datetimeoffset() | integer(). +-spec add_years(datetimeoffset(), integer()) -> datetimeoffset(). +-spec add_months(datetimeoffset(), integer()) -> datetimeoffset(). +-spec add_days(datetimeoffset(), number()) -> datetimeoffset(). +-spec add_hours(datetimeoffset(), number()) -> datetimeoffset(). +-spec add_minutes(datetimeoffset(), number()) -> datetimeoffset(). +-spec add_seconds(datetimeoffset(), number()) -> datetimeoffset(). +-spec add_milliseconds(datetimeoffset(), number()) -> datetimeoffset(). +-spec add_ticks(datetimeoffset(), integer()) -> datetimeoffset(). +-spec op_addition(datetimeoffset(), integer()) -> datetimeoffset(). +-spec op_subtraction(datetimeoffset(), datetimeoffset() | integer()) -> datetimeoffset() | integer(). +-spec compare(datetimeoffset(), datetimeoffset()) -> -1 | 0 | 1. +-spec equals(datetimeoffset(), datetimeoffset()) -> boolean(). +-spec equals_exact(datetimeoffset(), datetimeoffset()) -> boolean(). +-spec from_unix_time_seconds(integer()) -> datetimeoffset(). +-spec from_unix_time_milliseconds(integer()) -> datetimeoffset(). +-spec to_unix_time_seconds(datetimeoffset()) -> integer(). +-spec to_unix_time_milliseconds(datetimeoffset()) -> integer(). -spec try_parse(binary(), reference()) -> boolean(). -spec to_string(datetimeoffset()) -> binary(). -spec to_string(datetimeoffset(), binary()) -> binary(). @@ -55,14 +120,15 @@ %% - Ticks: same as DateTime (100-ns intervals from Jan 1, 0001) %% - Kind: 0=Unspecified, 1=UTC, 2=Local %% - OffsetTicks: offset from UTC as TimeSpan ticks -%% -%% For most property access, we delegate to fable_date since the -%% underlying datetime components are the same. -define(TICKS_PER_MILLISECOND, 10000). -define(TICKS_PER_SECOND, 10000000). -define(TICKS_PER_MINUTE, 600000000). -define(TICKS_PER_HOUR, 36000000000). +-define(TICKS_PER_DAY, 864000000000). + +%% .NET epoch offset: ticks from 0001-01-01 to 1970-01-01 +-define(UNIX_EPOCH_TICKS, 621355968000000000). %% ============================================================ %% Constructors @@ -72,24 +138,14 @@ create(Y, M, D, H, Min, S, OffsetTicks) when is_integer(OffsetTicks) -> DT = fable_date:create(Y, M, D, H, Min, S), {Ticks, _Kind} = DT, - Kind = - case OffsetTicks of - % UTC - 0 -> 1; - % Unspecified - _ -> 0 - end, + Kind = offset_to_kind(OffsetTicks), {Ticks, Kind, OffsetTicks}. %% create(Y, M, D, H, Min, S, Ms, OffsetTimeSpan) create(Y, M, D, H, Min, S, Ms, OffsetTicks) when is_integer(OffsetTicks) -> DT = fable_date:create(Y, M, D, H, Min, S, Ms), {Ticks, _Kind} = DT, - Kind = - case OffsetTicks of - 0 -> 1; - _ -> 0 - end, + Kind = offset_to_kind(OffsetTicks), {Ticks, Kind, OffsetTicks}. %% from_date(DateTime, OffsetTimeSpan) @@ -98,13 +154,19 @@ from_date({Ticks, Kind}, OffsetTicks) -> %% from_ticks(Ticks, OffsetTimeSpan) from_ticks(Ticks, OffsetTicks) -> - Kind = - case OffsetTicks of - 0 -> 1; - _ -> 0 - end, + Kind = offset_to_kind(OffsetTicks), {Ticks, Kind, OffsetTicks}. +%% from_date_time(DateOnly, TimeOnlyTicks, OffsetTimeSpan) +%% DateOnly is {DayTicks, Kind}, TimeOnly is ticks since midnight +from_date_time({DayTicks, _Kind}, TimeOnlyTicks, OffsetTicks) -> + Ticks = DayTicks + TimeOnlyTicks, + Kind = offset_to_kind(OffsetTicks), + {Ticks, Kind, OffsetTicks}. + +offset_to_kind(0) -> 1; % UTC +offset_to_kind(_) -> 0. % Unspecified + %% ============================================================ %% Properties (delegate to fable_date) %% ============================================================ @@ -116,6 +178,7 @@ hour({Ticks, Kind, _Offset}) -> fable_date:hour({Ticks, Kind}). minute({Ticks, Kind, _Offset}) -> fable_date:minute({Ticks, Kind}). second({Ticks, Kind, _Offset}) -> fable_date:second({Ticks, Kind}). millisecond({Ticks, Kind, _Offset}) -> fable_date:millisecond({Ticks, Kind}). +microsecond({Ticks, Kind, _Offset}) -> fable_date:microsecond({Ticks, Kind}). ticks({Ticks, _Kind, _Offset}) -> Ticks. %% Offset returns the TimeSpan offset (ticks) @@ -124,13 +187,35 @@ offset({_Ticks, _Kind, OffsetTicks}) -> OffsetTicks. %% DateTime property: returns a DateTime {Ticks, Kind=Unspecified} date_time({Ticks, _Kind, _Offset}) -> {Ticks, 0}. +%% Date property: returns DateTime with time zeroed +date({Ticks, Kind, _Offset}) -> fable_date:date({Ticks, Kind}). + +%% TimeOfDay: returns TimeSpan (ticks) of the time-of-day component +time_of_day({Ticks, _Kind, _Offset}) -> + Ticks rem ?TICKS_PER_DAY. + +%% DayOfWeek +day_of_week({Ticks, Kind, _Offset}) -> fable_date:day_of_week({Ticks, Kind}). + +%% DayOfYear +day_of_year({Ticks, Kind, _Offset}) -> fable_date:day_of_year({Ticks, Kind}). + +%% TotalOffsetMinutes +total_offset_minutes({_Ticks, _Kind, OffsetTicks}) -> + OffsetTicks div ?TICKS_PER_MINUTE. + +%% UtcTicks: Ticks adjusted by offset +utc_ticks({Ticks, _Kind, OffsetTicks}) -> + Ticks - OffsetTicks. + %% ============================================================ %% Static methods %% ============================================================ now() -> {Ticks, _Kind} = fable_date:now(), - {Ticks, 2, 0}. + OffsetTicks = local_offset_ticks(), + {Ticks, 2, OffsetTicks}. utc_now() -> {Ticks, _Kind} = fable_date:utc_now(), @@ -138,16 +223,125 @@ utc_now() -> min_value() -> {0, 0, 0}. +max_value() -> {3155378975999999999, 0, 0}. + +unix_epoch() -> + {?UNIX_EPOCH_TICKS, 1, 0}. + %% ============================================================ %% Conversion %% ============================================================ -to_local_time({Ticks, _Kind, _Offset}) -> - {Ticks, 2, 0}. +to_local_time({Ticks, _Kind, OffsetTicks}) -> + %% Convert to UTC ticks, then to local + UtcTicks = Ticks - OffsetTicks, + LocalOffset = local_offset_ticks(), + {UtcTicks + LocalOffset, 2, LocalOffset}. to_universal_time({Ticks, _Kind, OffsetTicks}) -> {Ticks - OffsetTicks, 1, 0}. +to_offset({Ticks, _Kind, OffsetTicks}, NewOffsetTicks) -> + %% Convert to UTC, then apply new offset + UtcTicks = Ticks - OffsetTicks, + Kind = offset_to_kind(NewOffsetTicks), + {UtcTicks + NewOffsetTicks, Kind, NewOffsetTicks}. + +local_date_time({Ticks, _Kind, OffsetTicks}) -> + UtcTicks = Ticks - OffsetTicks, + LocalOffset = local_offset_ticks(), + {UtcTicks + LocalOffset, 2}. + +utc_date_time({Ticks, _Kind, OffsetTicks}) -> + {Ticks - OffsetTicks, 1}. + +%% ============================================================ +%% Arithmetic +%% ============================================================ + +add({Ticks, Kind, OffsetTicks}, TimeSpanTicks) -> + {Ticks + TimeSpanTicks, Kind, OffsetTicks}. + +subtract({Ticks1, _Kind1, Offset1}, {Ticks2, _Kind2, Offset2}) -> + %% DTO - DTO -> TimeSpan (compare UTC instants) + (Ticks1 - Offset1) - (Ticks2 - Offset2); +subtract({Ticks, Kind, OffsetTicks}, TimeSpanTicks) when is_integer(TimeSpanTicks) -> + {Ticks - TimeSpanTicks, Kind, OffsetTicks}. + +add_years({Ticks, Kind, OffsetTicks}, Years) -> + {NewTicks, _} = fable_date:add_years({Ticks, Kind}, Years), + {NewTicks, Kind, OffsetTicks}. + +add_months({Ticks, Kind, OffsetTicks}, Months) -> + {NewTicks, _} = fable_date:add_months({Ticks, Kind}, Months), + {NewTicks, Kind, OffsetTicks}. + +add_days({Ticks, Kind, OffsetTicks}, Days) -> + Added = trunc(Days * ?TICKS_PER_DAY), + {Ticks + Added, Kind, OffsetTicks}. + +add_hours({Ticks, Kind, OffsetTicks}, Hours) -> + Added = trunc(Hours * ?TICKS_PER_HOUR), + {Ticks + Added, Kind, OffsetTicks}. + +add_minutes({Ticks, Kind, OffsetTicks}, Minutes) -> + Added = trunc(Minutes * ?TICKS_PER_MINUTE), + {Ticks + Added, Kind, OffsetTicks}. + +add_seconds({Ticks, Kind, OffsetTicks}, Seconds) -> + Added = trunc(Seconds * ?TICKS_PER_SECOND), + {Ticks + Added, Kind, OffsetTicks}. + +add_milliseconds({Ticks, Kind, OffsetTicks}, Milliseconds) -> + Added = trunc(Milliseconds * ?TICKS_PER_MILLISECOND), + {Ticks + Added, Kind, OffsetTicks}. + +add_ticks({Ticks, Kind, OffsetTicks}, AddedTicks) -> + {Ticks + AddedTicks, Kind, OffsetTicks}. + +op_addition(DTO, TimeSpanTicks) -> add(DTO, TimeSpanTicks). +op_subtraction(DTO, Other) -> subtract(DTO, Other). + +%% ============================================================ +%% Comparison / Equality +%% ============================================================ + +%% Compare by UTC instant +compare({Ticks1, _Kind1, Offset1}, {Ticks2, _Kind2, Offset2}) -> + Utc1 = Ticks1 - Offset1, + Utc2 = Ticks2 - Offset2, + if + Utc1 < Utc2 -> -1; + Utc1 > Utc2 -> 1; + true -> 0 + end. + +equals({Ticks1, _Kind1, Offset1}, {Ticks2, _Kind2, Offset2}) -> + (Ticks1 - Offset1) =:= (Ticks2 - Offset2). + +equals_exact({Ticks1, _Kind1, Offset1}, {Ticks2, _Kind2, Offset2}) -> + (Ticks1 - Offset1) =:= (Ticks2 - Offset2) andalso Offset1 =:= Offset2. + +%% ============================================================ +%% Unix time +%% ============================================================ + +from_unix_time_seconds(Seconds) -> + Ticks = ?UNIX_EPOCH_TICKS + Seconds * ?TICKS_PER_SECOND, + {Ticks, 1, 0}. + +from_unix_time_milliseconds(Ms) -> + Ticks = ?UNIX_EPOCH_TICKS + Ms * ?TICKS_PER_MILLISECOND, + {Ticks, 1, 0}. + +to_unix_time_seconds({Ticks, _Kind, OffsetTicks}) -> + UtcTicks = Ticks - OffsetTicks, + (UtcTicks - ?UNIX_EPOCH_TICKS) div ?TICKS_PER_SECOND. + +to_unix_time_milliseconds({Ticks, _Kind, OffsetTicks}) -> + UtcTicks = Ticks - OffsetTicks, + (UtcTicks - ?UNIX_EPOCH_TICKS) div ?TICKS_PER_MILLISECOND. + %% ============================================================ %% TryParse %% ============================================================ @@ -196,3 +390,19 @@ format_offset(OffsetTicks) -> H = AbsTicks div ?TICKS_PER_HOUR, M = (AbsTicks rem ?TICKS_PER_HOUR) div ?TICKS_PER_MINUTE, iolist_to_binary(io_lib:format("~s~2..0B:~2..0B", [Sign, H, M])). + +%% ============================================================ +%% Internal helpers +%% ============================================================ + +local_offset_ticks() -> + %% Get local offset from UTC in ticks + UtcSecs = erlang:system_time(second), + {{UY,UM,UD},{UH,UMin,US}} = calendar:now_to_universal_time({UtcSecs div 1000000, UtcSecs rem 1000000, 0}), + {{LY,LM,LD},{LH,LMin,LS}} = calendar:now_to_local_time({UtcSecs div 1000000, UtcSecs rem 1000000, 0}), + UtcDays = calendar:date_to_gregorian_days({UY, UM, UD}), + LocalDays = calendar:date_to_gregorian_days({LY, LM, LD}), + UtcSecsOfDay = UH * 3600 + UMin * 60 + US, + LocalSecsOfDay = LH * 3600 + LMin * 60 + LS, + DiffSecs = (LocalDays - UtcDays) * 86400 + (LocalSecsOfDay - UtcSecsOfDay), + DiffSecs * ?TICKS_PER_SECOND. diff --git a/src/fable-library-beam/fable_date_only.erl b/src/fable-library-beam/fable_date_only.erl new file mode 100644 index 0000000000..69c64e4e62 --- /dev/null +++ b/src/fable-library-beam/fable_date_only.erl @@ -0,0 +1,169 @@ +-module(fable_date_only). +-export([ + create/3, + min_value/0, + max_value/0, + year/1, + month/1, + day/1, + day_of_week/1, + day_of_year/1, + day_number/1, + from_day_number/1, + from_date_time/1, + add_days/2, + add_months/2, + add_years/2, + to_date_time/3, + to_string/1, to_string/2, to_string/3, + parse/1, + try_parse/2 +]). + +-type datetime() :: {integer(), 0 | 1 | 2}. + +-spec create(integer(), integer(), integer()) -> datetime(). +-spec min_value() -> datetime(). +-spec max_value() -> datetime(). +-spec year(datetime()) -> integer(). +-spec month(datetime()) -> integer(). +-spec day(datetime()) -> integer(). +-spec day_of_week(datetime()) -> integer(). +-spec day_of_year(datetime()) -> integer(). +-spec day_number(datetime()) -> integer(). +-spec from_day_number(integer()) -> datetime(). +-spec from_date_time(datetime()) -> datetime(). +-spec add_days(datetime(), integer()) -> datetime(). +-spec add_months(datetime(), integer()) -> datetime(). +-spec add_years(datetime(), integer()) -> datetime(). +-spec to_date_time(datetime(), integer(), integer()) -> datetime(). +-spec to_string(datetime()) -> binary(). +-spec to_string(datetime(), binary()) -> binary(). +-spec to_string(datetime(), binary(), term()) -> binary(). +-spec parse(binary()) -> datetime(). +-spec try_parse(binary(), reference()) -> boolean(). + +%% DateOnly is represented as a DateTime tuple {Ticks, Kind} +%% with time components always at midnight (00:00:00). +%% Kind is always 0 (Unspecified). + +-define(TICKS_PER_DAY, 864000000000). + +%% ============================================================ +%% Constructors +%% ============================================================ + +create(Y, M, D) -> + fable_date:create(Y, M, D). + +%% ============================================================ +%% Static fields +%% ============================================================ + +min_value() -> fable_date:create(1, 1, 1). + +max_value() -> fable_date:create(9999, 12, 31). + +%% ============================================================ +%% Properties +%% ============================================================ + +year(DT) -> fable_date:year(DT). +month(DT) -> fable_date:month(DT). +day(DT) -> fable_date:day(DT). +day_of_week(DT) -> fable_date:day_of_week(DT). +day_of_year(DT) -> fable_date:day_of_year(DT). + +day_number({Ticks, _Kind}) -> + Ticks div ?TICKS_PER_DAY. + +%% ============================================================ +%% Factory methods +%% ============================================================ + +from_day_number(DayNum) -> + Ticks = DayNum * ?TICKS_PER_DAY, + {Ticks, 0}. + +from_date_time({Ticks, _Kind}) -> + %% Strip time component + DayTicks = (Ticks div ?TICKS_PER_DAY) * ?TICKS_PER_DAY, + {DayTicks, 0}. + +%% ============================================================ +%% Arithmetic +%% ============================================================ + +add_days(DT, Days) -> + {Ticks, Kind} = fable_date:add_days(DT, Days), + %% Strip time just in case + DayTicks = (Ticks div ?TICKS_PER_DAY) * ?TICKS_PER_DAY, + {DayTicks, Kind}. + +add_months(DT, Months) -> + {Ticks, Kind} = fable_date:add_months(DT, Months), + DayTicks = (Ticks div ?TICKS_PER_DAY) * ?TICKS_PER_DAY, + {DayTicks, Kind}. + +add_years(DT, Years) -> + {Ticks, Kind} = fable_date:add_years(DT, Years), + DayTicks = (Ticks div ?TICKS_PER_DAY) * ?TICKS_PER_DAY, + {DayTicks, Kind}. + +%% ============================================================ +%% Conversion +%% ============================================================ + +%% to_date_time(DateOnly, TimeOnlyTicks, Kind) +%% TimeOnly is represented as ticks since midnight. +to_date_time({DayTicks, _Kind}, TimeOnlyTicks, NewKind) -> + {DayTicks + TimeOnlyTicks, NewKind}. + +%% ============================================================ +%% ToString +%% ============================================================ + +to_string(DT) -> to_string(DT, <<"d">>). + +to_string(DT, Format) -> to_string(DT, Format, undefined). + +to_string(DT, Format, _Provider) -> + Y = fable_date:year(DT), + M = fable_date:month(DT), + D = fable_date:day(DT), + YStr = pad(Y, 4), + MStr = pad(M, 2), + DStr = pad(D, 2), + case Format of + <<"d">> -> + iolist_to_binary([MStr, <<"/">>, DStr, <<"/">>, YStr]); + _ -> + %% "o" or "O" format + iolist_to_binary([YStr, <<"-">>, MStr, <<"-">>, DStr]) + end. + +pad(N, Width) -> + S = integer_to_binary(N), + Len = byte_size(S), + case Len >= Width of + true -> S; + false -> iolist_to_binary([binary:copy(<<"0">>, Width - Len), S]) + end. + +%% ============================================================ +%% Parse / TryParse +%% ============================================================ + +parse(Str) -> + %% Delegate to fable_date:parse and strip time + DT = fable_date:parse(Str), + from_date_time(DT). + +try_parse(Str, OutRef) -> + try + Result = parse(Str), + put(OutRef, Result), + true + catch + _:_ -> false + end. diff --git a/src/fable-library-beam/fable_time_only.erl b/src/fable-library-beam/fable_time_only.erl new file mode 100644 index 0000000000..46d9a7f30d --- /dev/null +++ b/src/fable-library-beam/fable_time_only.erl @@ -0,0 +1,233 @@ +-module(fable_time_only). +-export([ + create/3, create/4, + from_ticks/1, + from_time_span/1, + from_date_time/1, + min_value/0, + max_value/0, + hour/1, + minute/1, + second/1, + millisecond/1, + ticks/1, + add/2, + add_hours/2, + add_minutes/2, + is_between/3, + to_time_span/1, + to_string/1, to_string/2, to_string/3, + parse/1, + try_parse/2, + op_subtraction/2 +]). + +-type time_only() :: integer(). + +-spec create(integer(), integer(), integer()) -> time_only(). +-spec create(integer(), integer(), integer(), integer()) -> time_only(). +-spec from_ticks(integer()) -> time_only(). +-spec from_time_span(integer()) -> time_only(). +-spec from_date_time({integer(), integer()}) -> time_only(). +-spec min_value() -> time_only(). +-spec max_value() -> time_only(). +-spec hour(time_only()) -> integer(). +-spec minute(time_only()) -> integer(). +-spec second(time_only()) -> integer(). +-spec millisecond(time_only()) -> integer(). +-spec ticks(time_only()) -> integer(). +-spec add(time_only(), integer()) -> time_only(). +-spec add_hours(time_only(), number()) -> time_only(). +-spec add_minutes(time_only(), number()) -> time_only(). +-spec is_between(time_only(), time_only(), time_only()) -> boolean(). +-spec to_time_span(time_only()) -> integer(). +-spec to_string(time_only()) -> binary(). +-spec to_string(time_only(), binary()) -> binary(). +-spec to_string(time_only(), binary(), term()) -> binary(). +-spec parse(binary()) -> time_only(). +-spec try_parse(binary(), reference()) -> boolean(). +-spec op_subtraction(time_only(), time_only()) -> time_only(). + +%% TimeOnly is represented as ticks since midnight (integer), +%% same unit as TimeSpan. + +-define(TICKS_PER_MILLISECOND, 10000). +-define(TICKS_PER_SECOND, 10000000). +-define(TICKS_PER_MINUTE, 600000000). +-define(TICKS_PER_HOUR, 36000000000). +-define(TICKS_PER_DAY, 864000000000). + +%% ============================================================ +%% Constructors +%% ============================================================ + +create(H, M, S) -> + create(H, M, S, 0). + +create(H, M, S, Ms) -> + H * ?TICKS_PER_HOUR + M * ?TICKS_PER_MINUTE + + S * ?TICKS_PER_SECOND + Ms * ?TICKS_PER_MILLISECOND. + +from_ticks(T) -> T. + +from_time_span(TS) -> + case TS >= 0 andalso TS < ?TICKS_PER_DAY of + true -> TS; + false -> erlang:error(<<"The TimeSpan describes an unrepresentable TimeOnly.">>) + end. + +from_date_time({Ticks, _Kind}) -> + %% Extract time-of-day ticks + Ticks rem ?TICKS_PER_DAY. + +%% ============================================================ +%% Static fields +%% ============================================================ + +min_value() -> 0. + +max_value() -> ?TICKS_PER_DAY - 1. + +%% ============================================================ +%% Properties +%% ============================================================ + +hour(T) -> T div ?TICKS_PER_HOUR. + +minute(T) -> (T rem ?TICKS_PER_HOUR) div ?TICKS_PER_MINUTE. + +second(T) -> (T rem ?TICKS_PER_MINUTE) div ?TICKS_PER_SECOND. + +millisecond(T) -> (T rem ?TICKS_PER_SECOND) div ?TICKS_PER_MILLISECOND. + +ticks(T) -> T. + +%% ============================================================ +%% Arithmetic +%% ============================================================ + +add(T, TS) -> + T2 = (T + TS) rem ?TICKS_PER_DAY, + case T2 >= 0 of + true -> T2; + false -> ?TICKS_PER_DAY + T2 + end. + +add_hours(T, H) -> + add(T, trunc(H * ?TICKS_PER_HOUR)). + +add_minutes(T, M) -> + add(T, trunc(M * ?TICKS_PER_MINUTE)). + +%% ============================================================ +%% Queries +%% ============================================================ + +is_between(T, Start, End) -> + case Start =< End of + true -> Start =< T andalso End > T; + false -> Start =< T orelse End > T + end. + +%% ============================================================ +%% Conversion +%% ============================================================ + +to_time_span(T) -> T. + +%% ============================================================ +%% ToString +%% ============================================================ + +to_string(T) -> to_string(T, <<"t">>). + +to_string(T, Format) -> to_string(T, Format, undefined). + +to_string(T, Format, _Provider) -> + H = pad(hour(T), 2), + M = pad(minute(T), 2), + Base = iolist_to_binary([H, <<":">>, M]), + case Format of + <<"t">> -> Base; + <<"o">> -> long_format(T, Base); + <<"O">> -> long_format(T, Base); + _ -> + S = pad(second(T), 2), + iolist_to_binary([Base, <<":">>, S]) + end. + +long_format(T, Base) -> + S = pad(second(T), 2), + Ms = pad(millisecond(T), 3), + iolist_to_binary([Base, <<":">>, S, <<".">>, Ms, <<"0000">>]). + +pad(N, Width) -> + S = integer_to_binary(N), + Len = byte_size(S), + case Len >= Width of + true -> S; + false -> iolist_to_binary([binary:copy(<<"0">>, Width - Len), S]) + end. + +%% ============================================================ +%% Parse / TryParse +%% ============================================================ + +parse(Str) -> + %% Parse "HH:MM" or "HH:MM:SS" or "HH:MM:SS.fff" + case parse_time(Str) of + {ok, T} -> T; + error -> erlang:error(iolist_to_binary([<<"String '">>, Str, <<"' was not recognized as a valid TimeOnly.">>])) + end. + +try_parse(Str, OutRef) -> + case parse_time(Str) of + {ok, T} -> + put(OutRef, T), + true; + error -> + false + end. + +parse_time(Str) -> + %% Simple parser for HH:MM[:SS[.fff...]] + try + Parts = binary:split(Str, <<":">>, [global]), + case Parts of + [HB, MB] -> + {ok, create(binary_to_integer(HB), binary_to_integer(MB), 0, 0)}; + [HB, MB, Rest] -> + {SecB, Ms} = parse_seconds_frac(Rest), + {ok, create(binary_to_integer(HB), binary_to_integer(MB), + binary_to_integer(SecB), Ms)}; + _ -> error + end + catch + _:_ -> error + end. + +parse_seconds_frac(Rest) -> + case binary:split(Rest, <<".">>) of + [SecB] -> {SecB, 0}; + [SecB, FracB] -> + %% Normalize fraction to milliseconds + FracStr = binary_to_list(FracB), + Val = list_to_integer(FracStr), + Ms = case length(FracStr) of + 1 -> Val * 100; + 2 -> Val * 10; + 3 -> Val; + 4 -> Val div 10; + 5 -> Val div 100; + 6 -> Val div 1000; + _ -> list_to_integer(lists:sublist(FracStr, 7)) div 10000 + end, + {SecB, Ms} + end. + +%% ============================================================ +%% Operators +%% ============================================================ + +op_subtraction(Left, Right) -> + add(Left, -Right). diff --git a/tests/Beam/DateOnlyTests.fs b/tests/Beam/DateOnlyTests.fs new file mode 100644 index 0000000000..ce8c63addc --- /dev/null +++ b/tests/Beam/DateOnlyTests.fs @@ -0,0 +1,70 @@ +module Fable.Tests.DateOnlyTests + +open System +open Util.Testing + +[] +let ``test DateOnly constructors work`` () = + let d = DateOnly(2014, 10, 9) + d.Year |> equal 2014 + d.Month |> equal 10 + d.Day |> equal 9 + +[] +let ``test DateOnly.MinValue works`` () = + let d = DateOnly.MinValue + d.Year |> equal 1 + d.Month |> equal 1 + d.Day |> equal 1 + +[] +let ``test DateOnly.MaxValue works`` () = + let d = DateOnly.MaxValue + d.Year |> equal 9999 + d.Month |> equal 12 + d.Day |> equal 31 + +[] +let ``test DateOnly.DayOfWeek works`` () = + let d = DateOnly(2014, 10, 9) + d.DayOfWeek |> equal DayOfWeek.Thursday + +[] +let ``test DateOnly.DayOfYear works`` () = + let d = DateOnly(2014, 10, 9) + d.DayOfYear |> equal 282 + +[] +let ``test DateOnly.AddDays works`` () = + let d = DateOnly(2014, 10, 9) + let d2 = d.AddDays(10) + d2.Day |> equal 19 + d2.Month |> equal 10 + +[] +let ``test DateOnly.AddMonths works`` () = + let d = DateOnly(2014, 1, 31) + let d2 = d.AddMonths(1) + d2.Month |> equal 2 + d2.Day |> equal 28 + +[] +let ``test DateOnly.AddYears works`` () = + let d = DateOnly(2016, 2, 29) + let d2 = d.AddYears(1) + d2.Year |> equal 2017 + d2.Month |> equal 2 + d2.Day |> equal 28 + +[] +let ``test DateOnly.ToString works`` () = + let d = DateOnly(2014, 10, 9) + d.ToString("o", System.Globalization.CultureInfo.InvariantCulture) |> equal "2014-10-09" + +[] +let ``test DateOnly.FromDateTime works`` () = + let dt = DateTime(2014, 10, 9, 13, 23, 30) + let d = DateOnly.FromDateTime(dt) + d.Year |> equal 2014 + d.Month |> equal 10 + d.Day |> equal 9 diff --git a/tests/Beam/DateTimeOffsetTests.fs b/tests/Beam/DateTimeOffsetTests.fs index ae5d2cdd5f..e1892131fb 100644 --- a/tests/Beam/DateTimeOffsetTests.fs +++ b/tests/Beam/DateTimeOffsetTests.fs @@ -4,48 +4,140 @@ open System open Util.Testing open System.Globalization +let thatYearSeconds (dt: DateTimeOffset) = + (dt - DateTimeOffset(dt.Year, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds + +let thatYearMilliseconds (dt: DateTimeOffset) = + (dt - DateTimeOffset(dt.Year, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalMilliseconds + +// --------------------------------------------------------------------------- +// Constructors +// --------------------------------------------------------------------------- + [] -let ``test DateTimeOffset.TryParse works`` () = - let f (d: string) = - match DateTimeOffset.TryParse(d) with - | true, _ -> true - | false, _ -> false - f "foo" |> equal false - f "9/10/2014 1:50:34 PM" |> equal true - f "9/10/2014 1:50:34" |> equal true - f "2014-09-10T13:50:34" |> equal true +let ``test DateTimeOffset constructors work`` () = + let d3 = DateTimeOffset(2014, 10, 9, 13, 23, 30, TimeSpan.Zero) + let d5 = DateTimeOffset(2014, 10, 9, 13, 23, 30, 500, TimeSpan.Zero) + d3.Second + d5.Millisecond + |> equal 530 [] -let ``test DateTimeOffset.ToString() default works`` () = - let d = DateTimeOffset(2014, 10, 9, 13, 23, 30, TimeSpan.Zero) - let str = d.ToString() - str.Length > 0 |> equal true +let ``test DateTimeOffset from Year 1 to 99 works`` () = + let date = DateTimeOffset(1, 1, 2, 0, 0, 0, TimeSpan.Zero) + date.Year |> equal 1 + let date = DateTimeOffset(99, 1, 2, 0, 0, 0, TimeSpan.Zero) + date.Year |> equal 99 [] -let ``test DateTimeOffset.ToString with custom format works`` () = - DateTimeOffset(2014, 9, 11, 16, 37, 0, TimeSpan.Zero).ToString("HH:mm", CultureInfo.InvariantCulture) - |> equal "16:37" +let ``test DateTimeOffset constructor from Ticks works`` () = + let d = DateTimeOffset(624059424000000000L, TimeSpan.Zero) + equal 1978 d.Year + equal 7 d.Month + equal 27 d.Day [] -let ``test DateTimeOffset constructors work`` () = +let ``test DateTimeOffset constructor from DateTime works`` () = + let d1 = DateTime(2014, 10, 9, 13, 23, 30, DateTimeKind.Utc) + let d1' = DateTimeOffset(d1, TimeSpan.Zero) + equal d1.Year d1'.Year + equal d1.Day d1'.Day + equal d1.Hour d1'.Hour + +// --------------------------------------------------------------------------- +// Static fields +// --------------------------------------------------------------------------- + +[] +let ``test DateTimeOffset.MaxValue works`` () = + let d1 = DateTimeOffset.Now + let d2 = DateTimeOffset.MaxValue + d1 < d2 |> equal true + +[] +let ``test DateTimeOffset.MinValue works`` () = + let d1 = DateTimeOffset.Now + let d2 = DateTimeOffset.MinValue + d1 < d2 |> equal false + +[] +let ``test DateTimeOffset.UnixEpoch works`` () = + let epoch = DateTimeOffset.UnixEpoch + equal 1970 epoch.Year + equal 1 epoch.Month + equal 1 epoch.Day + +// --------------------------------------------------------------------------- +// Properties +// --------------------------------------------------------------------------- + +[] +let ``test DateTimeOffset.Now works`` () = + let d = DateTimeOffset.Now + d > DateTimeOffset.MinValue |> equal true + +[] +let ``test DateTimeOffset.UtcNow works`` () = + let d = DateTimeOffset.UtcNow + d > DateTimeOffset.MinValue |> equal true + +[] +let ``test DateTimeOffset.Hour works`` () = + let d = DateTimeOffset(2014, 10, 9, 13, 23, 30, TimeSpan.Zero) + d.Hour |> equal 13 + +[] +let ``test DateTimeOffset.Year works`` () = let d = DateTimeOffset(2014, 10, 9, 13, 23, 30, TimeSpan.Zero) d.Year |> equal 2014 + +[] +let ``test DateTimeOffset.Month works`` () = + let d = DateTimeOffset(2014, 10, 9, 13, 23, 30, TimeSpan.Zero) d.Month |> equal 10 + +[] +let ``test DateTimeOffset.Day works`` () = + let d = DateTimeOffset(2014, 10, 9, 13, 23, 30, TimeSpan.Zero) d.Day |> equal 9 - d.Hour |> equal 13 + +[] +let ``test DateTimeOffset.Minute works`` () = + let d = DateTimeOffset(2014, 10, 9, 13, 23, 30, TimeSpan.Zero) d.Minute |> equal 23 + +[] +let ``test DateTimeOffset.Second works`` () = + let d = DateTimeOffset(2014,9,12,0,0,30,TimeSpan.Zero) d.Second |> equal 30 - let d2 = DateTimeOffset(2014, 10, 9, 13, 23, 30, 500, TimeSpan.Zero) - d2.Second + d2.Millisecond |> equal 530 + +[] +let ``test DateTimeOffset.Millisecond works`` () = + let d = DateTimeOffset(2014, 10, 9, 13, 23, 30, 999, TimeSpan.Zero) + d.Millisecond |> equal 999 [] let ``test DateTimeOffset.Offset works`` () = let d1 = DateTimeOffset(2014, 10, 9, 13, 23, 30, TimeSpan.Zero) d1.Offset |> equal TimeSpan.Zero - let d2 = DateTimeOffset(2014, 10, 9, 13, 23, 30, TimeSpan.FromHours(3.0)) d2.Offset |> equal (TimeSpan.FromHours(3.0)) +[] +let ``test DateTimeOffset.DayOfWeek works`` () = + let d = DateTimeOffset(2014, 10, 9, 0, 0, 0, TimeSpan.Zero) + d.DayOfWeek |> equal DayOfWeek.Thursday + +[] +let ``test DateTimeOffset.DayOfYear works`` () = + let d = DateTimeOffset(2014, 10, 9, 0, 0, 0, TimeSpan.Zero) + d.DayOfYear |> equal 282 + +[] +let ``test DateTimeOffset.Date works`` () = + let d = DateTimeOffset(2014, 10, 9, 13, 23, 30, TimeSpan.Zero) + d.Date.Hour |> equal 0 + d.Date.Day |> equal 9 + [] let ``test DateTimeOffset.DateTime works`` () = let d = DateTimeOffset(2014, 10, 9, 13, 23, 30, TimeSpan.Zero) @@ -55,7 +147,311 @@ let ``test DateTimeOffset.DateTime works`` () = dt.Day |> equal 9 [] -let ``test DateTimeOffset is datetime compatible`` () = +let ``test DateTimeOffset.TimeOfDay works`` () = + let d = DateTimeOffset(2014, 10, 9, 13, 23, 30, 1, TimeSpan.Zero) + let t = d.TimeOfDay + t |> equal (TimeSpan(0, 13, 23, 30, 1)) + +[] +let ``test DateTimeOffset.Ticks does not care about offset`` () = + let d1 = DateTimeOffset(2014, 10, 9, 13, 23, 30, 500, TimeSpan.Zero) + let d2 = DateTimeOffset(2014, 10, 9, 13, 23, 30, 500, TimeSpan.FromHours 1.0) + let d3 = DateTimeOffset(2014, 10, 9, 13, 23, 30, 500, TimeSpan.FromHours -5.0) + equal d1.Ticks d2.Ticks + equal d1.Ticks d3.Ticks + equal d2.Ticks d3.Ticks + +[] +let ``test DateTimeOffset.Ticks works`` () = + let d = DateTimeOffset(2014, 10, 9, 13, 23, 30, 999, TimeSpan.Zero) + d.Ticks |> equal 635484578109990000L + let d = DateTimeOffset(2014, 10, 9, 13, 23, 30, 999, TimeSpan.FromMinutes(-150.)) + d.Ticks |> equal 635484578109990000L + +[] +let ``test DateTimeOffset.UtcTicks works`` () = + let d = DateTimeOffset(2014, 10, 9, 13, 23, 30, 999, TimeSpan.Zero) + d.UtcTicks |> equal 635484578109990000L + +// --------------------------------------------------------------------------- +// Equality / Comparison +// --------------------------------------------------------------------------- + +[] +let ``test DateTimeOffset equality cares about offset`` () = + let d1 = DateTimeOffset(2014, 10, 9, 13, 23, 30, 500, TimeSpan.Zero) + let d2 = DateTimeOffset(2014, 10, 9, 14, 23, 30, 500, TimeSpan.FromHours 1.0) + d1.Equals(d2) |> equal true + +[] +let ``test DateTimeOffset Comparison works`` () = + let test (ms: float) expected = + let dt1 = DateTimeOffset(2014, 10, 9, 13, 23, 30, 234, TimeSpan.Zero) + let dt2 = dt1.AddMilliseconds(ms) + let res1 = dt1.CompareTo(dt2) + let res2 = DateTimeOffset.Compare(dt1, dt2) + equal true (res1 = res2) + equal expected res1 + test 1000. -1 + test -1000. 1 + test 0. 0 + +[] +let ``test DateTimeOffset.EqualsExact works`` () = + let d1 = DateTimeOffset(2014, 10, 9, 13, 23, 30, 234, TimeSpan.Zero) + let d2 = DateTimeOffset(2014, 10, 9, 14, 23, 30, 234, TimeSpan.FromHours(1.)) + d1.Equals(d2) |> equal true + d1.EqualsExact(d2) |> equal false + +// --------------------------------------------------------------------------- +// Conversion +// --------------------------------------------------------------------------- + +[] +let ``test DateTimeOffset.ToLocalTime works`` () = + let d = DateTimeOffset.UtcNow + let localTime = d.ToLocalTime() + let utcTime = d.ToUniversalTime() + localTime.ToUniversalTime().Equals(utcTime) |> equal true + +[] +let ``test DateTimeOffset.ToUniversalTime works`` () = + let d = DateTimeOffset.Now + let localTime = d.ToLocalTime() + let utcTime = d.ToUniversalTime() + localTime.ToUniversalTime().Equals(utcTime) |> equal true + +[] +let ``test DateTimeOffset DateTime - Offset - UtcDateTime relationship`` () = + let dto = DateTimeOffset(2014, 10, 9, 13, 23, 30, 500, TimeSpan.FromHours -5.0) + let date = dto.DateTime + let utcDate = dto.UtcDateTime + let offset = dto.Offset + let compound = date - offset + // Compare by ticks since DateTime Kind may differ (Unspecified vs UTC) + equal compound.Ticks utcDate.Ticks + +[] +let ``test DateTimeOffset.LocalDateTime works`` () = let d = DateTimeOffset(2014, 10, 9, 13, 23, 30, TimeSpan.Zero) - d.Year |> equal 2014 - d.Month |> equal 10 + let d2 = d.LocalDateTime + equal d.Year d2.Year + equal d.Month d2.Month + equal d.Day d2.Day + +[] +let ``test DateTimeOffset.UtcDateTime works`` () = + let d = DateTimeOffset(2014, 10, 9, 13, 23, 30, TimeSpan.Zero) + let d2 = d.UtcDateTime + equal d.Year d2.Year + equal d.Month d2.Month + equal d.Day d2.Day + +// --------------------------------------------------------------------------- +// Unix time +// --------------------------------------------------------------------------- + +[] +let ``test DateTimeOffset.FromUnixTimeSeconds work`` () = + let d = DateTimeOffset.FromUnixTimeSeconds(1510310885L) + equal 2017 d.Year + equal 11 d.Month + equal 10 d.Day + +[] +let ``test DateTimeOffset.FromUnixTimeMilliseconds work`` () = + let d = DateTimeOffset.FromUnixTimeMilliseconds(1510310885000L) + equal 2017 d.Year + equal 11 d.Month + equal 10 d.Day + +[] +let ``test DateTimeOffset.ToUnixTimeSeconds works`` () = + let d = DateTimeOffset(2017, 11, 10, 0, 0, 0, TimeSpan.Zero) + d.ToUnixTimeSeconds() |> equal 1510272000L + +[] +let ``test DateTimeOffset.ToUnixTimeMilliseconds works`` () = + let d = DateTimeOffset(2017, 11, 10, 0, 0, 0, TimeSpan.Zero) + d.ToUnixTimeMilliseconds() |> equal 1510272000000L + +// --------------------------------------------------------------------------- +// Add methods +// --------------------------------------------------------------------------- + +[] +let ``test DateTimeOffset.AddYears works`` () = + let test v expected = + let dt = DateTimeOffset(2016,2,29,0,0,0,TimeSpan.Zero).AddYears(v) + equal expected (dt.Month + dt.Day) + test 100 31 + test 1 30 + test -1 30 + test -100 31 + test 0 31 + +[] +let ``test DateTimeOffset.AddMonths works`` () = + let test v expected = + let dt = DateTimeOffset(2016,1,31,0,0,0,TimeSpan.Zero).AddMonths(v) + dt.Year + dt.Month + dt.Day + |> equal expected + test 100 2060 + test 20 2056 + test 6 2054 + test 5 2052 + test 1 2047 + test 0 2048 + test -1 2058 + test -5 2054 + test -20 2050 + test -100 2046 + +[] +let ``test DateTimeOffset.AddDays works`` () = + let test v expected = + let dt = DateTimeOffset(2014,9,12,0,0,0,TimeSpan.Zero).AddDays(v) + thatYearSeconds dt + |> equal expected + test 100. 30585600.0 + test -100. 13305600.0 + test 0. 21945600.0 + +[] +let ``test DateTimeOffset.AddHours works`` () = + let test v expected = + let dt = DateTimeOffset(2014,9,12,0,0,0,TimeSpan.Zero).AddHours(v) + thatYearSeconds dt + |> equal expected + test 100. 22305600.0 + test -100. 21585600.0 + test 0. 21945600.0 + +[] +let ``test DateTimeOffset.AddMinutes works`` () = + let test v expected = + let dt = DateTimeOffset(2014,9,12,0,0,0,TimeSpan.Zero).AddMinutes(v) + thatYearSeconds dt + |> equal expected + test 100. 21951600.0 + test -100. 21939600.0 + test 0. 21945600.0 + +[] +let ``test DateTimeOffset.AddSeconds works`` () = + let test v expected = + let dt = DateTimeOffset(2014,9,12,0,0,0,TimeSpan.Zero).AddSeconds(v) + thatYearSeconds dt + |> equal expected + test 100. 21945700.0 + test -100. 21945500.0 + test 0. 21945600.0 + +[] +let ``test DateTimeOffset.AddMilliseconds works`` () = + let test v expected = + let dt = DateTimeOffset(2014,9,12,0,0,0,TimeSpan.Zero).AddMilliseconds(v) + thatYearMilliseconds dt + |> equal expected + test 100. 2.19456001e+10 + test -100. 2.19455999e+10 + test 0. 2.19456e+10 + +[] +let ``test DateTimeOffset.AddTicks works`` () = + let test v expected = + let dt = DateTimeOffset(2014,9,12,0,0,0,TimeSpan.Zero).AddTicks(v) + dt.Ticks + |> equal expected + let ticks = 635460768000000000L + test 100000L (ticks + 100000L) + test -100000L (ticks - 100000L) + test 0L ticks + +[] +let ``test DateTimeOffset Addition works`` () = + let test (ms: float) expected = + let dt = DateTimeOffset(2014,9,12,0,0,0,TimeSpan.Zero) + let ts = TimeSpan.FromMilliseconds(ms) + let res1 = dt.Add(ts) |> thatYearSeconds + let res2 = (dt + ts) |> thatYearSeconds + equal true (res1 = res2) + equal expected res1 + test 1000. 21945601.0 + test -1000. 21945599.0 + test 0. 21945600.0 + +// --------------------------------------------------------------------------- +// Subtract +// --------------------------------------------------------------------------- + +[] +let ``test DateTimeOffset Subtraction with TimeSpan works`` () = + let test (ms: float) expected = + let dt = DateTimeOffset(2014,9,12,0,0,0,TimeSpan.Zero) + let ts = TimeSpan.FromMilliseconds(ms) + let res1 = dt.Subtract(ts) |> thatYearSeconds + let res2 = (dt - ts) |> thatYearSeconds + equal true (res1 = res2) + equal expected res1 + test 1000. 21945599.0 + test -1000. 21945601.0 + test 0. 21945600.0 + +[] +let ``test DateTimeOffset Subtraction with DateTimeOffset works`` () = + let test (ms: float) expected = + let dt1 = DateTimeOffset(2014, 10, 9, 13, 23, 30, 234, TimeSpan.Zero) + let dt2 = dt1.AddMilliseconds(ms) + let res1 = dt1.Subtract(dt2).TotalSeconds + let res2 = (dt1 - dt2).TotalSeconds + equal true (res1 = res2) + equal expected res1 + test 1000. -1.0 + test -1000. 1.0 + test 0. 0.0 + +// --------------------------------------------------------------------------- +// Parse / ToString +// --------------------------------------------------------------------------- + +[] +let ``test DateTimeOffset.TryParse works`` () = + let f (d: string) = + match DateTimeOffset.TryParse(d) with + | true, _ -> true + | false, _ -> false + f "foo" |> equal false + f "9/10/2014 1:50:34 PM" |> equal true + f "9/10/2014 1:50:34" |> equal true + f "2014-09-10T13:50:34" |> equal true + +[] +let ``test DateTimeOffset.ToString() default works`` () = + let d = DateTimeOffset(2014, 10, 9, 13, 23, 30, TimeSpan.Zero) + let str = d.ToString() + str.Length > 0 |> equal true + +[] +let ``test DateTimeOffset.ToString with custom format works`` () = + DateTimeOffset(2014, 9, 11, 16, 37, 0, TimeSpan.Zero).ToString("HH:mm", CultureInfo.InvariantCulture) + |> equal "16:37" + +// --------------------------------------------------------------------------- +// DateOnly + TimeOnly constructor +// --------------------------------------------------------------------------- + +[] +let ``test DateTimeOffset constructor from DateOnly and TimeOnly works`` () = + let d = DateOnly(2014, 10, 9) + let t = TimeOnly(13, 23, 30, 500) + let o = TimeSpan.FromHours 5.0 + let dt = DateTimeOffset(d, t, o) + let dt_exp = DateTimeOffset(2014, 10, 9, 13, 23, 30, 500, o) + dt.Year |> equal dt_exp.Year + dt.Month |> equal dt_exp.Month + dt.Day |> equal dt_exp.Day + dt.Hour |> equal dt_exp.Hour + dt.Minute |> equal dt_exp.Minute + dt.Second |> equal dt_exp.Second + dt.Millisecond |> equal dt_exp.Millisecond diff --git a/tests/Beam/Fable.Tests.Beam.fsproj b/tests/Beam/Fable.Tests.Beam.fsproj index 35c862476a..0f098565f7 100644 --- a/tests/Beam/Fable.Tests.Beam.fsproj +++ b/tests/Beam/Fable.Tests.Beam.fsproj @@ -69,6 +69,8 @@ + + diff --git a/tests/Beam/TimeOnlyTests.fs b/tests/Beam/TimeOnlyTests.fs new file mode 100644 index 0000000000..5129769a96 --- /dev/null +++ b/tests/Beam/TimeOnlyTests.fs @@ -0,0 +1,61 @@ +module Fable.Tests.TimeOnlyTests + +open System +open Util.Testing + +[] +let ``test TimeOnly constructors work`` () = + let t = TimeOnly(13, 23, 30) + t.Hour |> equal 13 + t.Minute |> equal 23 + t.Second |> equal 30 + +[] +let ``test TimeOnly constructor with milliseconds works`` () = + let t = TimeOnly(13, 23, 30, 500) + t.Hour |> equal 13 + t.Minute |> equal 23 + t.Second |> equal 30 + t.Millisecond |> equal 500 + +[] +let ``test TimeOnly.MinValue works`` () = + let t = TimeOnly.MinValue + t.Hour |> equal 0 + t.Minute |> equal 0 + t.Second |> equal 0 + +[] +let ``test TimeOnly.ToTimeSpan works`` () = + let t = TimeOnly(13, 23, 30) + let ts = t.ToTimeSpan() + ts.Hours |> equal 13 + ts.Minutes |> equal 23 + ts.Seconds |> equal 30 + +[] +let ``test TimeOnly.AddHours works`` () = + let t = TimeOnly(13, 0, 0) + let t2 = t.AddHours(2.0) + t2.Hour |> equal 15 + +[] +let ``test TimeOnly.AddMinutes works`` () = + let t = TimeOnly(13, 30, 0) + let t2 = t.AddMinutes(45.0) + t2.Hour |> equal 14 + t2.Minute |> equal 15 + +[] +let ``test TimeOnly.IsBetween works`` () = + let t = TimeOnly(13, 0, 0) + let start = TimeOnly(12, 0, 0) + let finish = TimeOnly(14, 0, 0) + t.IsBetween(start, finish) |> equal true + t.IsBetween(finish, start) |> equal false + +[] +let ``test TimeOnly.ToString with format works`` () = + let t = TimeOnly(13, 23, 30) + t.ToString("T", System.Globalization.CultureInfo.InvariantCulture) |> equal "13:23:30" + t.ToString("t", System.Globalization.CultureInfo.InvariantCulture) |> equal "13:23"