From 3f13406eeb55f34a52e9bb1179c1c2553f634b55 Mon Sep 17 00:00:00 2001 From: Lexy Plt Date: Wed, 27 May 2026 18:27:58 +0200 Subject: [PATCH] feat(builtins): add two new builtins to manipulate dates --- CHANGELOG.md | 1 + include/Ark/Builtins/Builtins.hpp | 2 + lib/std | 2 +- src/arkreactor/Builtins/Builtins.cpp | 2 + src/arkreactor/Builtins/Time.cpp | 86 +++++++++++++++++++ .../CompilerSuite/ir/99bottles.expected | 4 +- .../CompilerSuite/ir/breakpoints.expected | 2 +- .../ir/operators_as_builtins.expected | 4 +- .../CompilerSuite/ir/plugin.expected | 2 +- .../optimized_ir/99bottles.expected | 12 +-- .../optimized_ir/builtins.expected | 2 +- .../builtin_timeparsedate_num.ark | 1 + .../builtin_timeparsedate_num.expected | 19 ++++ .../builtin_timeparsedate_str_num.ark | 1 + .../builtin_timeparsedate_str_num.expected | 20 +++++ .../typeChecking/builtin_timetodate.ark | 1 + .../typeChecking/builtin_timetodate.expected | 13 +++ 17 files changed, 160 insertions(+), 14 deletions(-) create mode 100644 tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timeparsedate_num.ark create mode 100644 tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timeparsedate_num.expected create mode 100644 tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timeparsedate_str_num.ark create mode 100644 tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timeparsedate_str_num.expected create mode 100644 tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timetodate.ark create mode 100644 tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timetodate.expected diff --git a/CHANGELOG.md b/CHANGELOG.md index 92d5dfe52..5ac977748 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - new builtin to extract the bytes of a string: builtin__string:bytes - math:fromBase to convert a number from a given base to base 10 - string:surrogate? and string:privateUse? to check if an unicode character is in one of those planes +- new builtins `builtin__time:timeToDate` and `builtin__time:parseDate` to convert a timestamp to a date, and parse a string date as a timestamp ### Changed - math:toBase handles 0 correctly diff --git a/include/Ark/Builtins/Builtins.hpp b/include/Ark/Builtins/Builtins.hpp index 15a998ba0..955e43774 100644 --- a/include/Ark/Builtins/Builtins.hpp +++ b/include/Ark/Builtins/Builtins.hpp @@ -62,6 +62,8 @@ namespace Ark::internal::Builtins namespace Time { ARK_BUILTIN(timeSinceEpoch); + ARK_BUILTIN(timestampToDate); + ARK_BUILTIN(parseDate); } namespace System diff --git a/lib/std b/lib/std index 8b5af97d9..12e63715e 160000 --- a/lib/std +++ b/lib/std @@ -1 +1 @@ -Subproject commit 8b5af97d9f46202c724f4b8a1aab9eb196d01680 +Subproject commit 12e63715e373dcdc7a7bb5597e6d1a09cff2c06f diff --git a/src/arkreactor/Builtins/Builtins.cpp b/src/arkreactor/Builtins/Builtins.cpp index 0e7e73f1c..ac398afe9 100644 --- a/src/arkreactor/Builtins/Builtins.cpp +++ b/src/arkreactor/Builtins/Builtins.cpp @@ -51,6 +51,8 @@ namespace Ark::internal::Builtins // Time { "time", Value(Time::timeSinceEpoch) }, + { "builtin__time:timeToDate", Value(Time::timestampToDate) }, + { "builtin__time:parseDate", Value(Time::parseDate) }, // System { "builtin__sys:platform", platform }, diff --git a/src/arkreactor/Builtins/Time.cpp b/src/arkreactor/Builtins/Time.cpp index b5d907e0a..f81549e03 100644 --- a/src/arkreactor/Builtins/Time.cpp +++ b/src/arkreactor/Builtins/Time.cpp @@ -1,7 +1,12 @@ #include +#include +#include +#include + #undef abs #include +#include #include @@ -15,6 +20,7 @@ namespace Ark::internal::Builtins::Time * =end * @author https://github.com/SuperFola */ + // cppcheck-suppress constParameterReference Value timeSinceEpoch(std::vector& n [[maybe_unused]], VM* vm [[maybe_unused]]) { const auto now = std::chrono::system_clock::now(); @@ -22,4 +28,84 @@ namespace Ark::internal::Builtins::Time const auto microseconds = std::chrono::duration_cast(epoch); return Value(static_cast(microseconds.count()) / 1000000); } + + Value timestampToDate(std::vector& n, VM* vm [[maybe_unused]]) + { + if (!types::check(n, ValueType::Number, ValueType::Any)) + throw types::TypeCheckingError( + "timeToDate", + { { types::Contract { + { types::Typedef("timestamp", ValueType::Number), + types::Typedef("utc?", ValueType::Any) } } } }, + n); + + const bool is_utc = n[1] == True; + const auto timestamp = std::chrono::milliseconds(static_cast(1000.0 * n[0].number())); + const auto timepoint = std::chrono::time_point(timestamp); + const std::time_t time = std::chrono::system_clock::to_time_t(timepoint); + const std::tm* calendar_time = is_utc ? std::gmtime(&time) : std::localtime(&time); + + if (calendar_time != nullptr) + { + internal::Dict dict; + dict.set(Value("millisecond"), Value(timestamp.count() % 1000)); + dict.set(Value("second"), Value(calendar_time->tm_sec)); + dict.set(Value("minute"), Value(calendar_time->tm_min)); + dict.set(Value("hour"), Value(calendar_time->tm_hour)); + dict.set(Value("day"), Value(calendar_time->tm_mday)); + dict.set(Value("month"), Value(calendar_time->tm_mon + 1)); + dict.set(Value("year"), Value(calendar_time->tm_year + 1900)); + dict.set(Value("week_day"), Value(calendar_time->tm_wday)); + dict.set(Value("year_day"), Value(calendar_time->tm_yday)); + dict.set(Value("is_dst"), calendar_time->tm_isdst ? True : False); + + return Value(std::move(dict)); + } + return Nil; + } + + int64_t makeTimestamp(const int tm_sec, const int tm_min, const int tm_hour, const int tm_mday, const int tm_mon, const int tm_year) + { + constexpr int MonthsPerYear = 12; + static const std::array cumulative_days = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 }; + + const long year = 1900 + tm_year + tm_mon / MonthsPerYear; + int64_t result = (year - 1970) * 365 + cumulative_days[static_cast(tm_mon % MonthsPerYear)]; + result += (year - 1968) / 4; + result -= (year - 1900) / 100; + result += (year - 1600) / 400; + if ((year % 4) == 0 && + ((year % 100) != 0 || (year % 400) == 0) && + (tm_mon % MonthsPerYear) < 2) + result--; + result += tm_mday - 1; + result *= 24; + result += tm_hour; + result *= 60; + result += tm_min; + result *= 60; + result += tm_sec; + return result; + } + + Value parseDate(std::vector& n, VM* vm [[maybe_unused]]) + { + if (!types::check(n, ValueType::String) && !types::check(n, ValueType::String, ValueType::String)) + throw types::TypeCheckingError( + "parseDate", + { { types::Contract { + { types::Typedef("date", ValueType::String) } }, + types::Contract { + { types::Typedef("date", ValueType::String), + types::Typedef("format", ValueType::String) } } } }, + n); + + std::tm t = {}; + std::istringstream ss(n[0].string()); + ss >> std::get_time(&t, n.size() == 1 ? "%Y-%m-%dT%H:%M:%S" : n[1].string().c_str()); + + if (ss.fail()) + return Nil; + return Value(makeTimestamp(t.tm_sec, t.tm_min, t.tm_hour, t.tm_mday, t.tm_mon, t.tm_year)); + } } diff --git a/tests/unittests/resources/CompilerSuite/ir/99bottles.expected b/tests/unittests/resources/CompilerSuite/ir/99bottles.expected index feea5ee20..0aa2c278f 100644 --- a/tests/unittests/resources/CompilerSuite/ir/99bottles.expected +++ b/tests/unittests/resources/CompilerSuite/ir/99bottles.expected @@ -37,7 +37,7 @@ page_0 LOAD_CONST 3 LOAD_FAST 4 LOAD_FAST 4 - CALL_BUILTIN 27, 3 + CALL_BUILTIN 29, 3 .L7: CALL_BUILTIN 9, 1 .L6: @@ -50,7 +50,7 @@ page_0 PUSH_RETURN_ADDRESS L9 LOAD_CONST 4 LOAD_FAST 4 - CALL_BUILTIN 27, 2 + CALL_BUILTIN 29, 2 .L9: CALL_BUILTIN 9, 1 .L8: diff --git a/tests/unittests/resources/CompilerSuite/ir/breakpoints.expected b/tests/unittests/resources/CompilerSuite/ir/breakpoints.expected index 8999a7f5d..d4e04adcf 100644 --- a/tests/unittests/resources/CompilerSuite/ir/breakpoints.expected +++ b/tests/unittests/resources/CompilerSuite/ir/breakpoints.expected @@ -62,7 +62,7 @@ page_1 LOAD_FAST_BY_INDEX 0 LOAD_FAST_BY_INDEX 1 LOAD_FAST_BY_INDEX 2 - CALL_BUILTIN 27, 4 + CALL_BUILTIN 29, 4 .L1: CALL_BUILTIN 9, 1 .L0: diff --git a/tests/unittests/resources/CompilerSuite/ir/operators_as_builtins.expected b/tests/unittests/resources/CompilerSuite/ir/operators_as_builtins.expected index 908810d01..af666e403 100644 --- a/tests/unittests/resources/CompilerSuite/ir/operators_as_builtins.expected +++ b/tests/unittests/resources/CompilerSuite/ir/operators_as_builtins.expected @@ -1,7 +1,5 @@ page_0 PUSH_RETURN_ADDRESS L0 - BUILTIN 87 - BUILTIN 88 BUILTIN 89 BUILTIN 90 BUILTIN 91 @@ -25,6 +23,8 @@ page_0 BUILTIN 109 BUILTIN 110 BUILTIN 111 + BUILTIN 112 + BUILTIN 113 CALL_BUILTIN 9, 25 .L0: POP 0 diff --git a/tests/unittests/resources/CompilerSuite/ir/plugin.expected b/tests/unittests/resources/CompilerSuite/ir/plugin.expected index 77c9c2126..1ee1867de 100644 --- a/tests/unittests/resources/CompilerSuite/ir/plugin.expected +++ b/tests/unittests/resources/CompilerSuite/ir/plugin.expected @@ -8,7 +8,7 @@ page_0 .L1: EQ 0 LOAD_CONST 3 - CALL_BUILTIN 26, 2 + CALL_BUILTIN 28, 2 .L0: POP 0 HALT 0 diff --git a/tests/unittests/resources/CompilerSuite/optimized_ir/99bottles.expected b/tests/unittests/resources/CompilerSuite/optimized_ir/99bottles.expected index 2131676dc..2227e444b 100644 --- a/tests/unittests/resources/CompilerSuite/optimized_ir/99bottles.expected +++ b/tests/unittests/resources/CompilerSuite/optimized_ir/99bottles.expected @@ -2,7 +2,7 @@ page_0 LOAD_CONST_STORE 0, 0 LOAD_CONST_STORE 1, 2 LOAD_CONST_STORE 2, 4 - BUILTIN 22 + BUILTIN 24 STORE 6 STORE_FROM 8, 7 STORE_FROM 10, 9 @@ -37,7 +37,7 @@ page_0 LOAD_CONST 6 LOAD_FAST 13 LOAD_FAST 13 - CALL_BUILTIN 27, 3 + CALL_BUILTIN 29, 3 .L10: CALL_BUILTIN 9, 1 .L9: @@ -47,7 +47,7 @@ page_0 PUSH_RETURN_ADDRESS L12 LOAD_CONST 7 LOAD_FAST 13 - CALL_BUILTIN 27, 2 + CALL_BUILTIN 29, 2 .L12: CALL_BUILTIN 9, 1 .L11: @@ -58,19 +58,19 @@ page_0 HALT 0 page_1 - CALL_BUILTIN_WITHOUT_RETURN_ADDRESS 23, 1 + CALL_BUILTIN_WITHOUT_RETURN_ADDRESS 25, 1 .L0: RET 0 HALT 0 page_2 - CALL_BUILTIN_WITHOUT_RETURN_ADDRESS 24, 1 + CALL_BUILTIN_WITHOUT_RETURN_ADDRESS 26, 1 .L1: RET 0 HALT 0 page_3 - CALL_BUILTIN_WITHOUT_RETURN_ADDRESS 25, 1 + CALL_BUILTIN_WITHOUT_RETURN_ADDRESS 27, 1 .L2: RET 0 HALT 0 diff --git a/tests/unittests/resources/CompilerSuite/optimized_ir/builtins.expected b/tests/unittests/resources/CompilerSuite/optimized_ir/builtins.expected index 8565951cf..3aa746ef3 100644 --- a/tests/unittests/resources/CompilerSuite/optimized_ir/builtins.expected +++ b/tests/unittests/resources/CompilerSuite/optimized_ir/builtins.expected @@ -5,7 +5,7 @@ page_0 HALT 0 page_1 - CALL_BUILTIN_WITHOUT_RETURN_ADDRESS 58, 1 + CALL_BUILTIN_WITHOUT_RETURN_ADDRESS 60, 1 .L0: RET 0 HALT 0 diff --git a/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timeparsedate_num.ark b/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timeparsedate_num.ark new file mode 100644 index 000000000..ead7929bc --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timeparsedate_num.ark @@ -0,0 +1 @@ +(builtin__time:parseDate 1) diff --git a/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timeparsedate_num.expected b/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timeparsedate_num.expected new file mode 100644 index 000000000..a8b447094 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timeparsedate_num.expected @@ -0,0 +1,19 @@ +Function parseDate expected between 1 argument and 2 arguments +Call + ↳ (parseDate 1) +Signature + ↳ (parseDate date) +Arguments + → `date' (expected String), got 1 (Number) + +Alternative 2: +Signature + ↳ (parseDate date format) +Arguments + → `date' (expected String), got 1 (Number) + → `format' (expected String) was not provided + +In file tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timeparsedate_num.ark:1 + 1 | (builtin__time:parseDate 1) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~ + 2 | diff --git a/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timeparsedate_str_num.ark b/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timeparsedate_str_num.ark new file mode 100644 index 000000000..0d9a15b85 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timeparsedate_str_num.ark @@ -0,0 +1 @@ +(builtin__time:parseDate "1" 1) diff --git a/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timeparsedate_str_num.expected b/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timeparsedate_str_num.expected new file mode 100644 index 000000000..8a5f2c9f2 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timeparsedate_str_num.expected @@ -0,0 +1,20 @@ +Function parseDate expected between 1 argument and 2 arguments +Call + ↳ (parseDate "1" 1) +Signature + ↳ (parseDate date) +Arguments + → `date' (expected String) ✓ + → unexpected additional args: 1 (Number) + +Alternative 2: +Signature + ↳ (parseDate date format) +Arguments + → `date' (expected String) ✓ + → `format' (expected String), got 1 (Number) + +In file tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timeparsedate_str_num.ark:1 + 1 | (builtin__time:parseDate "1" 1) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 2 | diff --git a/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timetodate.ark b/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timetodate.ark new file mode 100644 index 000000000..aff7f04e7 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timetodate.ark @@ -0,0 +1 @@ +(builtin__time:timeToDate "1" true) diff --git a/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timetodate.expected b/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timetodate.expected new file mode 100644 index 000000000..5080fda21 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timetodate.expected @@ -0,0 +1,13 @@ +Function timeToDate expected 2 arguments +Call + ↳ (timeToDate "1" true) +Signature + ↳ (timeToDate timestamp utc?) +Arguments + → `timestamp' (expected Number), got "1" (String) + → `utc?' (expected any) ✓ + +In file tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_timetodate.ark:1 + 1 | (builtin__time:timeToDate "1" true) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 2 |