diff --git a/CSV.ark b/CSV.ark new file mode 100644 index 0000000..df5dbe1 --- /dev/null +++ b/CSV.ark @@ -0,0 +1,15 @@ +(import std.Dict :fromList :get) +(import std.List :iota :zip) + +(let _makeCSV (fun (_data) { + (mut headers (head _data)) + (mut rows (tail _data)) + (let _headers_to_index (dict:fromList (list:zip headers (list:iota 0 (len headers))))) + + (fun (&_data &headers &_headers_to_index &rows) _data) })) + +(let readFile (fun (_filename _sep) ())) +(let read (fun (_data _sep) ())) +(let headers (fun (_csv) _csv.headers)) +(let rows (fun (_csv) _csv.rows)) +(let get (fun (_csv _column _row) (@@ _csv.rows _row (dict:get _csv._headers_to_index _column)))) diff --git a/Datetime.ark b/Datetime.ark new file mode 100644 index 0000000..5cb2f79 --- /dev/null +++ b/Datetime.ark @@ -0,0 +1,838 @@ +(import std.Dict) +(import std.Math :floordiv :round :abs) + +# @brief Dictionary of time zone offsets to UTC, in minutes +# =details-begin +# List obtained from https://github.com/vvo/tzdb/blob/ac6f4cbc6063b9a823a8ee1e4b5dffb6ca0a6c81/raw-time-zones.json, +# by keeping only `name` -> `rawOffsetInMinutes`. +# =details-end +# @author https://github.com/SuperFola +(let timezoneOffsets (dict + "Pacific/Midway" -660 + "Pacific/Pago_Pago" -660 + "Pacific/Niue" -660 + "Pacific/Rarotonga" -600 + "America/Adak" -600 + "Pacific/Honolulu" -600 + "Pacific/Tahiti" -600 + "Pacific/Marquesas" -570 + "America/Anchorage" -540 + "Pacific/Gambier" -540 + "America/Los_Angeles" -480 + "America/Tijuana" -480 + "America/Vancouver" -480 + "Pacific/Pitcairn" -480 + "America/Hermosillo" -420 + "America/Edmonton" -420 + "America/Ciudad_Juarez" -420 + "America/Denver" -420 + "America/Phoenix" -420 + "America/Whitehorse" -420 + "America/Belize" -360 + "America/Chicago" -360 + "America/Guatemala" -360 + "America/Managua" -360 + "America/Mexico_City" -360 + "America/Matamoros" -360 + "America/Costa_Rica" -360 + "America/El_Salvador" -360 + "America/Regina" -360 + "America/Tegucigalpa" -360 + "America/Winnipeg" -360 + "Pacific/Easter" -360 + "Pacific/Galapagos" -360 + "America/Rio_Branco" -300 + "America/Bogota" -300 + "America/Havana" -300 + "America/Atikokan" -300 + "America/Cancun" -300 + "America/Cayman" -300 + "America/Jamaica" -300 + "America/Nassau" -300 + "America/New_York" -300 + "America/Panama" -300 + "America/Port-au-Prince" -300 + "America/Grand_Turk" -300 + "America/Toronto" -300 + "America/Guayaquil" -300 + "America/Lima" -300 + "America/Manaus" -240 + "America/St_Kitts" -240 + "America/Blanc-Sablon" -240 + "America/Montserrat" -240 + "America/Barbados" -240 + "America/Port_of_Spain" -240 + "America/Martinique" -240 + "America/St_Lucia" -240 + "America/St_Barthelemy" -240 + "America/Halifax" -240 + "Atlantic/Bermuda" -240 + "America/St_Vincent" -240 + "America/Kralendijk" -240 + "America/Guadeloupe" -240 + "America/Marigot" -240 + "America/Aruba" -240 + "America/Lower_Princes" -240 + "America/Tortola" -240 + "America/Dominica" -240 + "America/St_Thomas" -240 + "America/Grenada" -240 + "America/Antigua" -240 + "America/Puerto_Rico" -240 + "America/Santo_Domingo" -240 + "America/Anguilla" -240 + "America/Thule" -240 + "America/Curacao" -240 + "America/La_Paz" -240 + "America/Santiago" -240 + "America/Guyana" -240 + "America/Caracas" -240 + "America/St_Johns" -210 + "America/Argentina/Buenos_Aires" -180 + "America/Sao_Paulo" -180 + "Antarctica/Palmer" -180 + "America/Punta_Arenas" -180 + "Atlantic/Stanley" -180 + "America/Cayenne" -180 + "America/Asuncion" -180 + "America/Miquelon" -180 + "America/Paramaribo" -180 + "America/Montevideo" -180 + "America/Noronha" -120 + "America/Nuuk" -120 + "Atlantic/South_Georgia" -120 + "Atlantic/Azores" -60 + "Atlantic/Cape_Verde" -60 + "Africa/Abidjan" 0 + "Africa/Bamako" 0 + "Africa/Bissau" 0 + "Africa/Conakry" 0 + "Africa/Dakar" 0 + "America/Danmarkshavn" 0 + "Europe/Isle_of_Man" 0 + "Europe/Dublin" 0 + "Africa/Freetown" 0 + "Atlantic/St_Helena" 0 + "Africa/Accra" 0 + "Africa/Lome" 0 + "Europe/London" 0 + "Africa/Monrovia" 0 + "Africa/Nouakchott" 0 + "Africa/Ouagadougou" 0 + "Atlantic/Reykjavik" 0 + "Europe/Jersey" 0 + "Europe/Guernsey" 0 + "Africa/Banjul" 0 + "Africa/Sao_Tome" 0 + "Antarctica/Troll" 0 + "Africa/Casablanca" 0 + "Africa/El_Aaiun" 0 + "Atlantic/Canary" 0 + "Europe/Lisbon" 0 + "Atlantic/Faroe" 0 + "Africa/Windhoek" 60 + "Africa/Algiers" 60 + "Europe/Andorra" 60 + "Europe/Belgrade" 60 + "Europe/Berlin" 60 + "Europe/Bratislava" 60 + "Europe/Brussels" 60 + "Europe/Budapest" 60 + "Europe/Copenhagen" 60 + "Europe/Gibraltar" 60 + "Europe/Ljubljana" 60 + "Arctic/Longyearbyen" 60 + "Europe/Luxembourg" 60 + "Europe/Madrid" 60 + "Europe/Monaco" 60 + "Europe/Oslo" 60 + "Europe/Paris" 60 + "Europe/Podgorica" 60 + "Europe/Prague" 60 + "Europe/Rome" 60 + "Europe/Amsterdam" 60 + "Europe/San_Marino" 60 + "Europe/Malta" 60 + "Europe/Sarajevo" 60 + "Europe/Skopje" 60 + "Europe/Stockholm" 60 + "Europe/Tirane" 60 + "Africa/Tunis" 60 + "Europe/Vaduz" 60 + "Europe/Vatican" 60 + "Europe/Vienna" 60 + "Europe/Warsaw" 60 + "Europe/Zagreb" 60 + "Europe/Zurich" 60 + "Africa/Bangui" 60 + "Africa/Malabo" 60 + "Africa/Brazzaville" 60 + "Africa/Porto-Novo" 60 + "Africa/Douala" 60 + "Africa/Kinshasa" 60 + "Africa/Lagos" 60 + "Africa/Libreville" 60 + "Africa/Luanda" 60 + "Africa/Ndjamena" 60 + "Africa/Niamey" 60 + "Africa/Bujumbura" 120 + "Africa/Gaborone" 120 + "Africa/Harare" 120 + "Africa/Juba" 120 + "Africa/Khartoum" 120 + "Africa/Kigali" 120 + "Africa/Blantyre" 120 + "Africa/Lubumbashi" 120 + "Africa/Lusaka" 120 + "Africa/Maputo" 120 + "Europe/Athens" 120 + "Asia/Beirut" 120 + "Europe/Bucharest" 120 + "Africa/Cairo" 120 + "Europe/Chisinau" 120 + "Asia/Hebron" 120 + "Europe/Helsinki" 120 + "Europe/Kaliningrad" 120 + "Europe/Kyiv" 120 + "Europe/Mariehamn" 120 + "Asia/Nicosia" 120 + "Europe/Riga" 120 + "Europe/Sofia" 120 + "Europe/Tallinn" 120 + "Africa/Tripoli" 120 + "Europe/Vilnius" 120 + "Asia/Jerusalem" 120 + "Africa/Johannesburg" 120 + "Africa/Mbabane" 120 + "Africa/Maseru" 120 + "Asia/Kuwait" 180 + "Asia/Bahrain" 180 + "Asia/Baghdad" 180 + "Asia/Qatar" 180 + "Asia/Riyadh" 180 + "Asia/Aden" 180 + "Asia/Amman" 180 + "Asia/Damascus" 180 + "Africa/Addis_Ababa" 180 + "Indian/Antananarivo" 180 + "Africa/Asmara" 180 + "Africa/Dar_es_Salaam" 180 + "Africa/Djibouti" 180 + "Africa/Kampala" 180 + "Indian/Mayotte" 180 + "Africa/Mogadishu" 180 + "Indian/Comoro" 180 + "Africa/Nairobi" 180 + "Europe/Minsk" 180 + "Europe/Moscow" 180 + "Europe/Simferopol" 180 + "Antarctica/Syowa" 180 + "Europe/Istanbul" 180 + "Asia/Tehran" 210 + "Asia/Yerevan" 240 + "Asia/Baku" 240 + "Asia/Tbilisi" 240 + "Asia/Dubai" 240 + "Asia/Muscat" 240 + "Indian/Mauritius" 240 + "Indian/Reunion" 240 + "Europe/Samara" 240 + "Indian/Mahe" 240 + "Asia/Kabul" 270 + "Indian/Kerguelen" 300 + "Asia/Almaty" 300 + "Indian/Maldives" 300 + "Antarctica/Mawson" 300 + "Asia/Karachi" 300 + "Asia/Dushanbe" 300 + "Asia/Ashgabat" 300 + "Asia/Tashkent" 300 + "Asia/Yekaterinburg" 300 + "Asia/Colombo" 330 + "Asia/Kolkata" 330 + "Asia/Kathmandu" 345 + "Asia/Dhaka" 360 + "Asia/Thimphu" 360 + "Asia/Urumqi" 360 + "Indian/Chagos" 360 + "Asia/Bishkek" 360 + "Asia/Omsk" 360 + "Indian/Cocos" 390 + "Asia/Yangon" 390 + "Indian/Christmas" 420 + "Antarctica/Davis" 420 + "Asia/Bangkok" 420 + "Asia/Ho_Chi_Minh" 420 + "Asia/Phnom_Penh" 420 + "Asia/Vientiane" 420 + "Asia/Hovd" 420 + "Asia/Novosibirsk" 420 + "Asia/Jakarta" 420 + "Antarctica/Casey" 480 + "Australia/Perth" 480 + "Asia/Brunei" 480 + "Asia/Makassar" 480 + "Asia/Macau" 480 + "Asia/Shanghai" 480 + "Asia/Hong_Kong" 480 + "Asia/Irkutsk" 480 + "Asia/Kuala_Lumpur" 480 + "Asia/Manila" 480 + "Asia/Singapore" 480 + "Asia/Taipei" 480 + "Asia/Ulaanbaatar" 480 + "Australia/Eucla" 525 + "Asia/Jayapura" 540 + "Asia/Tokyo" 540 + "Asia/Pyongyang" 540 + "Asia/Seoul" 540 + "Pacific/Palau" 540 + "Asia/Dili" 540 + "Asia/Chita" 540 + "Australia/Adelaide" 570 + "Australia/Darwin" 570 + "Australia/Brisbane" 600 + "Australia/Sydney" 600 + "Pacific/Guam" 600 + "Pacific/Saipan" 600 + "Pacific/Chuuk" 600 + "Antarctica/DumontDUrville" 600 + "Pacific/Port_Moresby" 600 + "Asia/Vladivostok" 600 + "Australia/Lord_Howe" 630 + "Pacific/Bougainville" 660 + "Pacific/Kosrae" 660 + "Asia/Sakhalin" 660 + "Pacific/Noumea" 660 + "Pacific/Norfolk" 660 + "Pacific/Guadalcanal" 660 + "Pacific/Efate" 660 + "Pacific/Fiji" 720 + "Pacific/Tarawa" 720 + "Asia/Kamchatka" 720 + "Pacific/Majuro" 720 + "Pacific/Nauru" 720 + "Pacific/Auckland" 720 + "Antarctica/McMurdo" 720 + "Pacific/Funafuti" 720 + "Pacific/Wake" 720 + "Pacific/Wallis" 720 + "Pacific/Chatham" 765 + "Pacific/Kanton" 780 + "Pacific/Apia" 780 + "Pacific/Fakaofo" 780 + "Pacific/Tongatapu" 780 + "Pacific/Kiritimati" 840)) + +# internal, do not use +(let _cumulativeDays [0 31 59 90 120 151 181 212 243 273 304 334]) + +# @brief Get the offset to UTC of a time zone by name, in minutes +# @details Abort if the timezone is not known +# @param _tz String, time zone name +# @author https://github.com/SuperFola +(let utcOffsetMinutes (fun (_tz) { + (let _offset (dict:get timezoneOffsets _tz)) + (assert (not (nil? _offset)) "Unknown timezone") + _offset })) + +# @brief Construct a UTC timestamp +# =details-begin +# This doesn't handle the timezone changes from 1970 and before, +# it only uses modern ones which is good enough in most cases. +# =details-end +# @param _year Number +# @param _month Number, in [1, 12] range +# @param _day Number, in [1, 31] range +# @param _hour Number, in [0, 23] range +# @param _minute Number, in [0, 59] range +# @param _second Number, in [0, 60] range +# @param _millisecond Number, in [0, 999] range +# @param _tz String representing a time zone ; can be nil to indicate UTC +# @param _dst? Bool, true if the given date is in day light saving, false otherwise +# =begin +# (print (datetime:makeUTCTimestamp 2026 5 27 14 28 5 300 "Europe/Paris" true)) +# # 1779884885.3 +# =end +# @author https://github.com/SuperFola +(let makeUTCTimestamp (fun ((mut _year) (mut _month) _day _hour _minute _second _millisecond _tz _dst?) { + (assert (and (>= _day 1) (<= _day 31)) "Day must be in [1-31]") + (assert (and (>= _month 1) (<= _month 12)) "Month must be in [1-12]") + (assert (and (>= _hour 0) (<= _hour 23)) "Hour must be in [0-23]") + (assert (and (>= _minute 0) (<= _minute 59)) "Minute must be in [0-59]") + (assert (and (>= _second 0) (<= _second 60)) "Second must be in [0-60]") + + # _month is between 1 and 12, this algorithm needs it between 0 and 11 + (set _month (- _month 1)) + (set _year (+ _year (floordiv _month 12))) + + (mut _result (+ (* (- _year 1970) 365) (@ _cumulativeDays (mod _month 12)))) + (set _result (+ _result (floordiv (- _year 1968) 4) (* -1 (floordiv (- _year 1900) 100)) (floordiv (- _year 1600) 400))) + (if (and (= 0 (mod _year 4)) (or (!= 0 (mod _year 100)) (= 0 (mod _year 400))) (< (mod _month 12) 2)) + (set _result (- _result 1))) + (let _tzOffset + (if (nil? _tz) + 0 + (* -1 (utcOffsetMinutes _tz)))) + (+ + (* + (+ + (* + (+ + (* (+ _result _day -1) 24) + _hour + (if _dst? + -1 + 0)) + 60) + _minute + _tzOffset) + 60) + _second + (/ _millisecond 1000)) })) + +# @brief Precomputed timestamp of 1/1/0000 at 00H 00M 00.000s +# @author https://github.com/SuperFola +(let year0 (makeUTCTimestamp 0 1 1 0 0 0 0 nil false)) + +# @brief Precomputed timestamp of 1/1/1970 at 00H 00M 00.000s +# @author https://github.com/SuperFola +(let year1970 0) + +# @brief Precomputed timestamp of 1/1/2000 at 00H 00M 00.000s +# @author https://github.com/SuperFola +(let year2000 (makeUTCTimestamp 2000 1 1 0 0 0 0 nil false)) + +# @brief Convert a date to a UTC timestamp +# =details-begin +# This doesn't handle the timezone changes from 1970 and before, +# it only uses modern ones which is good enough in most cases. +# +# Keys needed in the Dict: +# - `millisecond` - milliseconds after the second – [0, 999] +# - `second` - seconds after the minute – [0, 60] +# - `minute` - minutes after the hour – [0, 59] +# - `hour` - hours since midnight – [0, 23] +# - `day` - day of the month – [1, 31] +# - `month` - months since January – [1, 12] +# - `year` - years since 0 +# - `week_day` - days since Sunday – [0, 6] +# - `year_day` - days since January 1 – [0, 365] +# - `is_dst` - Daylight Saving Time flag. The value is true if DST is in effect, false if not or if no information is available. +# =details-end +# @param _date Dict, as returned by `datetime:asUTCDate` +# =begin +# (let t (time)) +# (print t) # 1779908523.389969 +# (print (datetime:toUTCTimestamp (datetime:asUTCDate t)) # 1779908523.389 +# =end +# @author https://github.com/SuperFola +(let toUTCTimestamp (fun (_date) (makeUTCTimestamp + (dict:get _date "year") + (dict:get _date "month") + (dict:get _date "day") + (dict:get _date "hour") + (dict:get _date "minute") + (dict:get _date "second") + (dict:get _date "millisecond") + nil + (dict:get _date "is_dst")))) + +# @brief Convert a timestamp to a UTC date +# @param _time timestamp +# =details-begin +# Returns a Dict with the following keys: +# - `millisecond` - milliseconds after the second – [0, 999] +# - `second` - seconds after the minute – [0, 60] +# - `minute` - minutes after the hour – [0, 59] +# - `hour` - hours since midnight – [0, 23] +# - `day` - day of the month – [1, 31] +# - `month` - months since January – [1, 12] +# - `year` - years since 0 +# - `week_day` - days since Sunday – [0, 6] +# - `year_day` - days since January 1 – [0, 365] +# - `is_dst` - Daylight Saving Time flag. The value is true if DST is in effect, false if not or if no information is available. +# =details-end +# =begin +# (print (datetime:asUTCDate (time))) +# # {millisecond: 913, second: 24, minute: 15, hour: 17, day: 26, month: 5, year: 2026, week_day: 2, year_day: 145, is_dst: false} +# =end +# @author https://github.com/SuperFola +(let asUTCDate (fun (_time) (builtin__time:asUTCDate _time))) + +# @brief Add a number of seconds to a timestamp +# @param _time Number, timestamp +# @param _quantity Number, quantity of the unit to add +# @author https://github.com/SuperFola +(let plusSeconds (fun (_time _quantity) (+ _time _quantity))) + +# @brief Subtract a number of seconds to a timestamp +# @param _time Number, timestamp +# @param _quantity Number, quantity of the unit to remove +# @author https://github.com/SuperFola +(let minusSeconds (fun (_time _quantity) (- _time _quantity))) + +# @brief Add a number of minutes to a timestamp +# @param _time Number, timestamp +# @param _quantity Number, quantity of the unit to add +# @author https://github.com/SuperFola +(let plusMinutes (fun (_time _quantity) (+ _time (* 60 _quantity)))) + +# @brief Subtract a number of minutes to a timestamp +# @param _time Number, timestamp +# @param _quantity Number, quantity of the unit to remove +# @author https://github.com/SuperFola +(let minusMinutes (fun (_time _quantity) (- _time (* 60 _quantity)))) + +# @brief Add a number of hours to a timestamp +# @param _time Number, timestamp +# @param _quantity Number, quantity of the unit to add +# @author https://github.com/SuperFola +(let plusHours (fun (_time _quantity) (+ _time (* 3600 _quantity)))) + +# @brief Subtract a number of hours to a timestamp +# @param _time Number, timestamp +# @param _quantity Number, quantity of the unit to remove +# @author https://github.com/SuperFola +(let minusHours (fun (_time _quantity) (- _time (* 3600 _quantity)))) + +# @brief Add a number of days to a timestamp +# @param _time Number, timestamp +# @param _quantity Number, quantity of the unit to add +# @author https://github.com/SuperFola +(let plusDays (fun (_time _quantity) (+ _time (* 86400 _quantity)))) + +# @brief Subtract a number of days to a timestamp +# @param _time Number, timestamp +# @param _quantity Number, quantity of the unit to remove +# @author https://github.com/SuperFola +(let minusDays (fun (_time _quantity) (- _time (* 86400 _quantity)))) + +# @brief Add a number of weeks to a timestamp +# @param _time Number, timestamp +# @param _quantity Number, quantity of the unit to add +# @author https://github.com/SuperFola +(let plusWeeks (fun (_time _quantity) (+ _time (* 604800 _quantity)))) + +# @brief Subtract a number of weeks to a timestamp +# @param _time Number, timestamp +# @param _quantity Number, quantity of the unit to remove +# @author https://github.com/SuperFola +(let minusWeeks (fun (_time _quantity) (- _time (* 604800 _quantity)))) + +# @brief Add a number of seconds to a timestamp +# @details A month is considered to be a fixed 30 days +# @param _time Number, timestamp +# @param _quantity Number, quantity of the unit to add +# @author https://github.com/SuperFola +(let plusMonths (fun (_time _quantity) (+ _time (* 2592000 _quantity)))) + +# @brief Subtract a number of months to a timestamp +# @details A month is considered to be a fixed 30 days +# @param _time Number, timestamp +# @param _quantity Number, quantity of the unit to remove +# @author https://github.com/SuperFola +(let minusMonths (fun (_time _quantity) (- _time (* 2592000 _quantity)))) + +# @brief Add a number of seconds to a timestamp +# @details A year is considered to be a fixed 365 days +# @param _time Number, timestamp +# @param _quantity Number, quantity of the unit to add +# @author https://github.com/SuperFola +(let plusYears (fun (_time _quantity) (+ _time (* 31536000 _quantity)))) + +# @brief Subtract a number of years to a timestamp +# @details A year is considered to be a fixed 365 days +# @param _time Number, timestamp +# @param _quantity Number, quantity of the unit to remove +# @author https://github.com/SuperFola +(let minusYears (fun (_time _quantity) (- _time (* 31536000 _quantity)))) + +# @brief Put a given timestamp at the start of the day, at 00H 00M 00.000s +# @param _time Number, timestamp +# @author https://github.com/SuperFola +(let atStartOfDay (fun (_time) { + (let _date (asUTCDate _time)) + (dict:update! + _date + (dict + "millisecond" 0 + "second" 0 + "minute" 0 + "hour" 0)) + (toUTCTimestamp _date) })) + +# @brief Put a given timestamp at the end of the day, at 23H 59M 59.999s +# @param _time Number, timestamp +# @author https://github.com/SuperFola +(let atEndOfDay (fun (_time) { + (let _date (asUTCDate _time)) + (dict:update! + _date + (dict + "millisecond" 999 + "second" 59 + "minute" 59 + "hour" 23)) + (toUTCTimestamp _date) })) + +# @brief Compute today's date, at 00H 00M 00s +# @details Returns a timestamp +# @author https://github.com/SuperFola +(let today (fun () (atStartOfDay (time)))) + +# @brief Compute yesterday's date, at 00H 00M 00s +# @details Returns a timestamp +# @author https://github.com/SuperFola +(let yesterday (fun () (atStartOfDay (minusDays (time) 1)))) + +# @brief Compute tomorrow's date, at 00H 00M 00s +# @details Returns a timestamp +# @author https://github.com/SuperFola +(let tomorrow (fun () (atStartOfDay (plusDays (time) 1)))) + +# @brief Return the timestamp of the next day of a given timestamp, keeping the hours, minutes, seconds and milliseconds +# @param _time Number, timestamp +# @author https://github.com/SuperFola +(let nextDay (fun (_time) (plusDays _time 1))) + +# @brief Return the timestamp of the previous day of a given timestamp, keeping the hours, minutes, seconds and milliseconds +# @param _time Number, timestamp +# @author https://github.com/SuperFola +(let previousDay (fun (_time) (minusDays _time 1))) + +# @brief Compute the delta between two timestamps +# =details-begin +# Output Dict has the following keys: +# - milliseconds +# - seconds +# - minutes +# - hours +# - days +# =details-end +# @param _t1 Number, first timestamp +# @param _t2 Number, second timestamp +# @author https://github.com/SuperFola +(let delta (fun (_t1 _t2) { + (let _t (- _t2 _t1)) + + (mut _days (floordiv _t 86400)) + (if (< (mod _t 3600) 0) + (set _days (- _days 1))) + + (dict + "milliseconds" (millisecond _t) + "seconds" (second _t) + "minutes" (minute _t) + "hours" (hour _t) + "days" _days) })) + +# @brief Create a delta from a number of seconds, as a Dict with the same shape as `datetime:asUTCDate` +# @param _time Number, number of seconds +# @author https://github.com/SuperFola +(let asDelta (fun (_time) (delta 0 _time))) + +# @brief Convert a delta or a date to a number of seconds +# @param _date Dict, with the same shape as `datetime:asUTCDate` +# @author https://github.com/SuperFola +(let asSeconds (fun (_date) + (if (nil? (dict:get _date "days")) + (- (toUTCTimestamp _date) year0) + { + (+ + (/ (dict:get _date "milliseconds") 1000) + (dict:get _date "seconds") + (* 60 (dict:get _date "minutes")) + (* 3600 (dict:get _date "hours")) + (* 86400 (dict:get _date "days"))) }))) + +# @brief Add a delta to a timestamp +# @param _time Number, timestamp +# @param _delta Dict, with the same shape as `datetime:asUTCDate` +# @author https://github.com/SuperFola +(let plusDelta (fun (_time _delta) (+ _time (asSeconds _delta)))) + +# @brief Subtract a delta to a timestamp +# @param _time Number, timestamp +# @param _delta Dict, with the same shape as `datetime:asUTCDate` +# @author https://github.com/SuperFola +(let minusDelta (fun (_time _delta) (- _time (asSeconds _delta)))) + +# @brief Return the year component of a timestamp +# @param _time Number, timestamp +# @author https://github.com/SuperFola +(let year (fun (_time) (dict:get (asUTCDate _time) "year"))) + +# @brief Return the month (1-12) component of a timestamp +# @param _time Number, timestamp +# @author https://github.com/SuperFola +(let month (fun (_time) (dict:get (asUTCDate _time) "month"))) + +# @brief Return the day (1-31) of the month component of a timestamp +# @param _time Number, timestamp +# @author https://github.com/SuperFola +(let day (fun (_time) (dict:get (asUTCDate _time) "day"))) + +# @brief Return the hour (0-23) component of a timestamp +# @param _time Number, timestamp +# @author https://github.com/SuperFola +(let hour (fun (_time) (floordiv (mod _time 86400) 3600))) + +# @brief Return the minute (0-59) component of a timestamp +# @param _time Number, timestamp +# @author https://github.com/SuperFola +(let minute (fun (_time) (floordiv (mod _time 3600) 60))) + +# @brief Return the second (0-59) component of a timestamp +# @param _time Number, timestamp +# @author https://github.com/SuperFola +(let second (fun (_time) (builtin__math:floor (mod _time 60)))) + +# @brief Return the millisecond (0-999) component of a timestamp +# @param _time Number, timestamp +# @author https://github.com/SuperFola +(let millisecond (fun (_time) (round (* 1000 (- _time (builtin__math:floor _time)))))) + +# @brief Return the day of the week component of a timestamp (starts at 0 for monday, 6 for sunday) +# @param _time Number, timestamp +# @author https://github.com/SuperFola +(let dayOfWeek (fun (_time) (dict:get (asUTCDate _time) "week_day"))) + +# @brief Return the day of the year (0-365) component of a timestamp +# @param _time Number, timestamp +# @author https://github.com/SuperFola +(let dayOfYear (fun (_time) (dict:get (asUTCDate _time) "year_day"))) + +# @brief Check if a year is a leap year +# @param _year Number, year +# @author https://github.com/SuperFola +(let leapYear? (fun (_year) { + (or (and (= 0 (mod _year 4)) (!= 0 (mod _year 100))) (= 0 (mod _year 400))) })) + +# @brief Return the number of days in the month a timestamp is at +# @param _time Number, timestamp +# @author https://github.com/SuperFola +(let monthLength (fun (_time) { + (let _month (month _time)) + (if (or (= 1 _month) (= 3 _month) (= 5 _month) (= 7 _month) (= 8 _month) (= 10 _month) (= 12 _month)) + 31 + (if (or (= 4 _month) (= 6 _month) (= 9 _month) (= 11 _month)) + 30 + (if (leapYear? (year _time)) + 29 + 28))) })) + +# @brief Return the number of days in the year a timestamp is at +# @param _time Number, timestamp +# @author https://github.com/SuperFola +(let yearLength (fun (_time) + (if (leapYear? (year _time)) + 366 + 365))) + +# @brief Convert a timezone to its representation: +05:45 +# @details Abort if the timezone is not known +# @param _tz String, timezone +# @author https://github.com/SuperFola +(let utcOffsetRepr (fun (_tz) { + (let _minutes (utcOffsetMinutes _tz)) + (assert (not (nil? _minutes)) "Unknown timezone") + (let _hoursToUtc (abs (floordiv _minutes 60))) + (let _minToUtc (mod _minutes 60)) + (format + "{}{:02}:{:02}" + (if (< _minutes 0) + "-" + "+") + _hoursToUtc + _minToUtc) })) + +# @brief Convert a timestamp to its ISO8601 representation: 2007-04-05T12:34:56.789Z +# @param _time Number, timestamp +# @author https://github.com/SuperFola +(let asISO8601 (fun (_time) { + (let _data (asUTCDate _time)) + (format + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z" + (dict:get _data "year") + (dict:get _data "month") + (dict:get _data "day") + (dict:get _data "hour") + (dict:get _data "minute") + (dict:get _data "second") + (dict:get _data "millisecond")) })) + +# @brief Try to parse a date as `%Y-%m-%dT%H:%M:%S` and return it as a timestamp, or `nil` if it fails +# @param _str String, date following `%Y-%m-%dT%H:%M:%S` +# @author https://github.com/SuperFola +(let parse (fun (_str) (builtin__time:parseDate _str))) + +# @brief Try to parse a date as a given format and return it as a timestamp, or `nil` if it fails +# =details-begin +# Format spec for `_format`: +# - `%` matches a literal `%`. The full conversion specification must be `%%` +# - `t` matches any whitespace +# - `n` matches any whitespace +# +# **Year** +# - `Y` parses full year as a 4 digit decimal number, leading zeroes permitted but not required +# - `EY` parses year in the alternative representation, e.g. `平成23年` (year Heisei 23) which writes 2011 to year in ja_JP locale +# - `y` parses last 2 digits of year as a decimal number. Range [69,99] results in values 1969 to 1999, range [00,68] results in 2000-2068 +# - `Oy` parses last 2 digits of year using the alternative numeric system, e.g. `十一` is parsed as 11 in ja_JP locale +# - `Ey` parses year as offset from locale's alternative calendar period `%EC` +# - `C` parses the first 2 digits of year as a decimal number (range [00,99]) +# - `EC` parses the name of the base year (period) in the locale's alternative representation, e.g. `平成` (Heisei era) in ja_JP +# +# **Month** +# - `b` parses the month name, either full or abbreviated, e.g. `Oct` +# - `h` synonym of `b` +# - `B` synonym of `b` +# - `m` parses the month as a decimal number (range [01,12]), leading zeroes permitted but not required +# - `Om` parses the month using the alternative numeric system, e.g. `十二` parses as 12 in ja_JP locale +# +# **Week** +# - `U` parses the week of the year as a decimal number (Sunday is the first day of the week) (range [00,53]), leading zeroes permitted but not required +# - `OU` parses the week of the year, as by `%U`, using the alternative numeric system, e.g. `五十二` parses as 52 in ja_JP locale +# - `W` parses the week of the year as a decimal number (Monday is the first day of the week) (range [00,53]), leading zeroes permitted but not required +# - `OW` parses the week of the year, as by `%W`, using the alternative numeric system, e.g. `五十二` parses as 52 in ja_JP locale +# +# **Day of the year/month** +# - `j` parses day of the year as a decimal number (range [001,366]), leading zeroes permitted but not required +# - `d` parses the day of the month as a decimal number (range [01,31]), leading zeroes permitted but not required +# - `Od` parses the day of the month using the alternative numeric system, e.g. `二十七` parses as 27 in ja_JP locale, leading zeroes permitted but not required +# - `e` synonym of `d` +# - `Oe` synonym of `Od` +# +# **Day of the week** +# - `a` parses the name of the day of the week, either full or abbreviated, e.g. `Fri` +# - `A` synonym of `a` +# - `w` parses weekday as a decimal number, where Sunday is 0 (range [0-6]) +# - `Ow` parses weekday as a decimal number, where Sunday is 0, using the alternative numeric system, e.g. `二` parses as 2 in ja_JP locale +# +# **Hour, minute, second** +# - `H` parses the hour as a decimal number, 24 hour clock (range [00-23]), leading zeroes permitted but not required +# - `OH` parses hour from 24-hour clock using the alternative numeric system, e.g. `十八` parses as 18 in ja_JP locale +# - `I` parses hour as a decimal number, 12 hour clock (range [01,12]), leading zeroes permitted but not required +# - `OI` parses hour from 12-hour clock using the alternative numeric system, e.g. `六` reads as 06 in ja_JP locale +# - `M` parses minute as a decimal number (range [00,59]), leading zeroes permitted but not required +# - `OM` parses minute using the alternative numeric system, e.g. `二十五` parses as 25 in ja_JP locale +# - `S` parses second as a decimal number (range [00,60]), leading zeroes permitted but not required +# - `OS` parses second using the alternative numeric system, e.g. `二十四` parses as 24 in ja_JP locale +# +# **Other** +# - `c` parses the locale's standard date and time string format, e.g. `Sun Oct 17 04:41:13 2010` (locale dependent) +# - `Ec` parses the locale's alternative date and time string format, e.g. expecting `平成23年` (year Heisei 23) instead of `2011年` (year 2011) in ja_JP locale +# - `x` parses the locale's standard date representation +# - `Ex` parses the locale's alternative date representation, e.g. expecting `平成23年` (year Heisei 23) instead of `2011年` (year 2011) in ja_JP locale +# - `X` parses the locale's standard time representation +# - `EX` parses the locale's alternative time representation +# - `D` equivalent to `"%m / %d / %y "` +# - `r` parses locale's standard 12-hour clock time (in POSIX, `"%I : %M : %S %p"`) +# - `R` equivalent to `"%H : %M"` +# - `T` equivalent to `"%H : %M : %S"` +# - `p` parses the locale's equivalent of `a.m.` or `p.m.` +# =details-end +# @param _str String, date +# @param _format String, follows [std::get_time](https://en.cppreference.com/cpp/io/manip/get_time) format +# @author https://github.com/SuperFola +(let parseAs (fun (_str _format) (builtin__time:parseDate _str _format))) diff --git a/Dict.ark b/Dict.ark index 40b0407..0ef2aef 100644 --- a/Dict.ark +++ b/Dict.ark @@ -120,6 +120,23 @@ (set _i (+ 1 _i)) }) _output })) +# @brief Create a list from a list of pairs (lists of 2 elements, key and value) +# @param _L list +# =begin +# (let data [["a" 1] ["b" 2] ["c" 3]]) +# (let new (dict:fromList data)) +# (print new) # {a: 1, b:2, c:3} +# =end +# @author https://github.com/SuperFola +(let fromList (fun ((ref _L)) { + (mut _output (dict)) + (mut _i 0) + (while (< _i (len _L)) { + (assert (= 2 (len (@ _L _i))) "Expected a pair [key value]") + (add _output (@@ _L _i 0) (@@ _L _i 1)) + (set _i (+ 1 _i)) }) + _output })) + # @brief Map each value in a dictionary with a given function # @details The original dictionary is not modified # @param _D dictionary diff --git a/List.ark b/List.ark index 7676a15..20b5a8b 100644 --- a/List.ark +++ b/List.ark @@ -1,3 +1,5 @@ +(import std.Math) + # @brief Reverse a given list and return a new one # @details The original list is not modified # @param list the list to reverse @@ -8,7 +10,7 @@ (let reverse (fun (_L) (builtin__list:reverse _L))) # @brief Search an element in a List -# @details The original list is not modified +# @details The original list is not modified. Return -1 when not found # @param list the List to search in # @param value the element to search # =begin @@ -18,6 +20,147 @@ # @author https://github.com/SuperFola (let find (fun (_L _x) (builtin__list:find _L _x))) +# @brief Search an element in a List +# @details The original List is not modified. Return -1 when not found +# @param list the List to search in +# @param x the element to search for +# @param startIndex index to start searching from +# =begin +# (list:findAfter [1 2 3] 1 0) # 0 +# (list:findAfter [1 2 3] 1 1) # -1 +# =end +# @author https://github.com/SuperFola +(let findAfter (fun (_L _x _idx) (builtin__list:find _L _x _idx))) + +# @brief Search an element in a List with a predicate +# @details The original List is not modified. Return nil when not found +# @param list the List to search in +# @param pred unary Function returning a boolean +# =begin +# (list:findIf [1 2 3] math:even?) # 1 +# (list:findIf [1 3 5] math:even?) # nil +# =end +# @author https://github.com/SuperFola +(let findIf (fun (_L _f) { + (mut _i 0) + (mut _output nil) + (while (< _i (len _L)) { + (if (_f (@ _L _i)) + { + (set _output _i) + (set _i (len _L)) }) + (set _i (+ 1 _i)) }) + _output })) + +# @brief Search for a set of contiguous elements inside a list +# @details The original List is not modified. Return nil when not found +# @param list the List to search in +# @param sub the elements to search for +# =begin +# (list:search [1 2 3 4 5 6] [2 3 4]) # 1 +# (list:search [1 2 3 4 5 6] [2 4]) # nil +# =end +# @author https://github.com/SuperFola +(let search (fun ((ref _L) (ref _s)) { + (assert (not (empty? _s)) "The sublist to look for can't be empty") + (mut _i (find _L (head _s))) + (if (!= -1 _i) + { + (mut _continue true) + (mut _output nil) + + (while _continue { + (mut _ii _i) + (mut _j 0) + (mut _ok true) + (while (and _ok (< _j (len _s)) (< _ii (len _L))) + (if (!= (@ _L _ii) (@ _s _j)) + (set _ok false) + { + (set _j (+ 1 _j)) + (set _ii (+ 1 _ii)) })) + (if (and _ok (= _j (len _s))) + { + (set _continue false) + (set _output _i) }) + (if (>= _i (len _L)) + (set _continue false)) + (set _i (+ 1 _i)) }) + _output } + nil) })) + +# @brief Search for the first element in the list which is not ordered before value and return its index +# @details The original List is not modified. Expect the List to be sorted. Return nil when not found +# @param list the List to search in +# @param x element to search for +# =begin +# (let prices [100.0 101.5 102.5 102.5 107.3]) +# (list:lowerBound prices 102.5) # 2 +# (list:lowerBound prices 110) # nil +# =end +# @author https://github.com/SuperFola +(let lowerBound (fun ((ref _L) _x) { + (mut _it nil) + (mut _count (len _L)) + (mut _first 0) + + (while (> _count 0) { + (let _step (math:floordiv _count 2)) + (set _it (+ _first _step)) + (if (and (< _it (len _L)) (< (@ _L _it) _x)) + { + (set _it (+ 1 _it)) + (set _first _it) + (set _count (+ (- _count _step) 1)) } + (set _count _step)) }) + + (if (< _first (len _L)) + _first + nil) })) + +# @brief Search for the first element in the list which is ordered after value and return its index +# @details The original List is not modified. Expect the List to be sorted. Return nil when not found +# @param list the List to search in +# @param x element to search for +# =begin +# (let prices [100.0 101.5 102.5 102.5 107.3]) +# (list:upperBound prices 102.5) # 4 +# (list:upperBound prices 110) # nil +# =end +# @author https://github.com/SuperFola +(let upperBound (fun ((ref _L) _x) { + (mut _it nil) + (mut _count (len _L)) + (mut _first 0) + + (while (> _count 0) { + (let _step (math:floordiv _count 2)) + (set _it (+ _first _step)) + (if (and (< _it (len _L)) (>= _x (@ _L _it))) + { + (set _it (+ 1 _it)) + (set _first _it) + (set _count (+ (- _count _step) 1)) } + (set _count _step)) }) + + (if (< _first (len _L)) + _first + nil) })) + +# @brief Check if an element is in a sorted List +# @details The original List is not modified. Expect the List to be sorted +# @param list the List to search in +# @param x element to search for +# =begin +# (let prices [1 3 5 9]) +# (list:binarySearch prices 1) # true +# (list:binarySearch prices 2) # false +# =end +# @author https://github.com/SuperFola +(let binarySearch (fun ((ref _L) _x) { + (let _first (lowerBound _L _x)) + (and (< _first (len _L)) (>= _x (@ _L _first))) })) + # @brief Search if an element is in a List # @details The original list is not modified # @param list the List to search in @@ -62,6 +205,28 @@ # @author https://github.com/SuperFola (let sort (fun (_L) (builtin__list:sort _L))) +# @brief Check if a List is sorted +# @details The original list is not modified +# @param list List to check +# =begin +# (list:sorted? [4 2 3]) # false +# (list:sorted? []) # true +# (list:sorted? [1]) # true +# (list:sorted? [1 2 5]) # true +# =end +# @author https://github.com/SuperFola +(let sorted? (fun ((ref _L)) { + (mut _i 0) + (mut _ok true) + + (while (and _ok (< _i (- (len _L) 1))) { + (set _ok (<= (@ _L _i) (@ _L (+ 1 _i)))) + (set _i (+ 1 _i)) }) + + (if (<= (len _L) 1) + true + _ok) })) + # @brief Generate a List of n copies of an element # @param count the number of copies # @param value the element to copy @@ -263,7 +428,28 @@ (set _index (+ 1 _index)) }) _output })) -(import std.Math :min :max :even?) +# @brief Find the minimum and the maximum in a list of numbers +# @param _L list of numbers +# @details The original list is not modified. +# =begin +# (let value (list:minMax [0 1 2 3 5 8])) # [0 8] +# =end +# @author https://github.com/SuperFola +(let minMax (fun ((ref _L)) { + (mut _index 0) + (mut _min nil) + (mut _max nil) + + (while (< _index (len _L)) { + (let _val (@ _L _index)) + + (if (or (nil? _min) (< _val _min)) + (set _min _val)) + (if (or (nil? _max) (> _val _max)) + (set _max _val)) + + (set _index (+ 1 _index)) }) + [_min _max] })) # @brief Find the median in a list of numbers # @param _L list of numbers @@ -320,8 +506,7 @@ (while (< _index (len _L)) { (let _result (_f (@ _L _index))) - (if (not (nil? _result)) - (append! _output _result)) + (if (not (nil? _result)) (append! _output _result)) (set _index (+ 1 _index)) }) _output })) @@ -558,6 +743,44 @@ (set _index (+ 1 _index)) }))) _output })) +# @brief Shift the elements of a List to the left, putting the overflow at the end +# @param _L the list to work on +# @param _c Number of elements to move (must be positive) +# @details The original list is not modified. +# =begin +# (list:shiftLeft [1 2 3 4 5] 2) # [3 4 5 1 2] +# =end +# @author https://github.com/SuperFola +(let shiftLeft (fun ((ref _L) (mut _c)) { + (assert (>= _c 0) "count must be positive") + (set _c (mod _c (len _L))) + + (if (or (empty? _L) (= 0 _c)) + _L + { + (let _to_shift (first _L _c)) + (let _tail (last _L (- (len _L) _c))) + (concat _tail _to_shift) }) })) + +# @brief Shift the elements of a List to the right, putting the overflow at the beginning +# @param _L the list to work on +# @param _c Number of elements to move (must be positive) +# @details The original list is not modified. +# =begin +# (list:shiftRight [1 2 3 4 5] 2) # [4 5 1 2 3] +# =end +# @author https://github.com/SuperFola +(let shiftRight (fun ((ref _L) (mut _c)) { + (assert (>= _c 0) "count must be positive") + (set _c (mod _c (len _L))) + + (if (or (empty? _L) (= 0 _c)) + _L + { + (let _to_shift (last _L _c)) + (let _head (first _L (- (len _L) _c))) + (concat _to_shift _head) }) })) + # @brief Partition a list in two, given a predicate # @param _L the list to work on # @param _f the predicate, accepting the value and its index diff --git a/Macros.ark b/Macros.ark index 4915b0e..559577b 100644 --- a/Macros.ark +++ b/Macros.ark @@ -132,6 +132,26 @@ (let outx ($as-is (@ pair 0))) (let outy ($as-is (@ pair 1))) }) +# internal, do not use +(macro __unpack (data idx name ...names) { + (let name (@ data idx)) + ($if (> ($len names) 0) (__unpack data (+ 1 idx) ...names)) }) + +# @brief Unpack a list into multiple variables +# @param data list of elements to unpack +# @param ...names variable names to use +# =begin +# (let data [6 22 19 25 5 2026 2]) +# (unpack data second minute hour day month year timezone) +# (print (format "date: {:02}:{:02}:{:02} {}/{:02}/{} {:+03}:00" hour minute second day month year timezone)) +# # date: 19:22:06 25/05/2026 +02:00 +# =end +# @author https://github.com/SuperFola +(macro unpack (data ...names) { + (macro var ($gensym)) + (let var data) + (__unpack var 0 ...names) }) + # @brief Increment a variable, by generating a `set` # @param value symbol to increment # =begin diff --git a/Prelude.ark b/Prelude.ark index fdd85bc..9c08d96 100644 --- a/Prelude.ark +++ b/Prelude.ark @@ -1,3 +1,4 @@ +(import std.Datetime) (import std.Dict) (import std.IO) (import std.List) diff --git a/String.ark b/String.ark index cfbb01b..ad1da85 100644 --- a/String.ark +++ b/String.ark @@ -155,10 +155,7 @@ (let privateUse? (fun (_str) { (assert (= 1 (utf8len _str)) "Expected a single character") (let _c (ord _str)) - (or - (and (<= 57344 _c) (<= _c 63743)) - (and (<= 983040 _c) (<= _c 1048573)) - (and (<= 1048576 _c) (<= _c 1114109))) })) + (or (and (<= 57344 _c) (<= _c 63743)) (and (<= 983040 _c) (<= _c 1048573)) (and (<= 1048576 _c) (<= _c 1114109))) })) # @brief Check if a string contains a word # @param _str String where the lookup occurs @@ -322,18 +319,7 @@ (and (>= _startingIndex 0) (< _startingIndex (len _string))) "slice start index must be in range [0, string length[") - (mut _returnedString "") - (mut _index _startingIndex) - - (let _end - (if (>= _length (len _string)) - (len _string) - (+ _index _length))) - - (while (< _index _end) { - (set _returnedString (+ _returnedString (@ _string _index))) - (set _index (+ _index 1)) }) - _returnedString }))) + (builtin__slice _string _startingIndex (+ _startingIndex _length)) }))) # @brief Split a string in multiple substrings in a list, given a separator # @param _string the string to split diff --git a/tests/all.ark b/tests/all.ark index a2b19e6..3af133a 100644 --- a/tests/all.ark +++ b/tests/all.ark @@ -1,4 +1,5 @@ (import cli-tests) +(import datetime-tests) (import dict-tests) (import events-tests) (import exceptions-tests) @@ -18,6 +19,7 @@ (let outputs (list:unzip [ cli-tests:cli-output + datetime-tests:datetime-output dict-tests:dict-output events-tests:events-output exceptions-tests:exceptions-output diff --git a/tests/datetime-tests.ark b/tests/datetime-tests.ark new file mode 100644 index 0000000..91f7c51 --- /dev/null +++ b/tests/datetime-tests.ark @@ -0,0 +1,257 @@ +(import std.Datetime) +(import std.Testing) + +(test:suite datetime { + (let now (time)) + # use fixed timestamps to avoid computing a new month/year + (mut t (datetime:makeUTCTimestamp 2020 2 15 6 2 4 0 nil false)) + (mut t0229 (datetime:makeUTCTimestamp 2020 2 29 6 2 4 0 nil false)) + (mut t0301 (datetime:makeUTCTimestamp 2020 3 1 6 2 4 0 nil false)) + (let oneMinuteAfterY0 (dict "millisecond" 0 "second" 0 "minute" 1 "hour" 0 "day" 1 "month" 1 "year" 0 "week_day" 5 "year_day" 0 "is_dst" false)) + (let oneMinuteAfterY0Delta (dict "milliseconds" 0 "seconds" 0 "minutes" 1 "hours" 0 "days" 0)) + + (test:case "utcOffsetMinutes" { + (test:eq (datetime:utcOffsetMinutes "Europe/Paris") 60) + (test:eq (datetime:utcOffsetMinutes "Pacific/Midway") -660) + (test:eq (datetime:utcOffsetMinutes "Pacific/Kiritimati") 840) }) + + (test:case "makeUTCTimestamp" { + (test:eq (datetime:makeUTCTimestamp 1970 1 1 0 0 0 0 nil false) 0) + (test:eq (datetime:makeUTCTimestamp 1000 1 1 0 0 0 0 nil false) -30610224000) + (test:eq (datetime:makeUTCTimestamp 400 1 1 0 0 0 0 nil false) -49544438400) + (test:eq (datetime:makeUTCTimestamp 0 1 1 0 0 0 0 nil false) -62167219200) + (test:eq (datetime:makeUTCTimestamp 4000 1 1 0 0 0 0 nil false) 64060588800) + (test:eq (datetime:makeUTCTimestamp 2020 2 29 6 2 4 0 nil false) 1582956124) + + (mut utc (datetime:makeUTCTimestamp 2026 5 28 6 2 4 0 nil false)) + (test:eq utc 1779948124) + + (mut local (datetime:makeUTCTimestamp 2026 5 28 6 2 4 0 "Europe/Paris" true)) + (test:eq local 1779940924) + # Europe/Paris with DST is 2 hours ahead of UTC + (test:eq utc (+ 7200 local)) }) + + (test:case "constants" { + (test:eq datetime:year0 -62167219200) + (test:eq datetime:year1970 0) + (test:eq datetime:year2000 946684800) }) + + (test:case "toUTCTimestamp" { + (test:eq (datetime:toUTCTimestamp oneMinuteAfterY0) (+ 60 datetime:year0)) + (test:eq (datetime:toUTCTimestamp (datetime:asUTCDate datetime:year0)) datetime:year0) + (test:eq (datetime:toUTCTimestamp (datetime:asUTCDate datetime:year1970)) datetime:year1970) + (test:eq (datetime:toUTCTimestamp (datetime:asUTCDate datetime:year2000)) datetime:year2000) }) + + (test:case "asUTCDate" { + (test:eq (datetime:asUTCDate 1779948124) (dict "millisecond" 0 "second" 4 "minute" 2 "hour" 6 "day" 28 "month" 5 "year" 2026 "week_day" 3 "year_day" 147 "is_dst" false)) + (test:eq (datetime:asUTCDate 1582956124) (dict "millisecond" 0 "second" 4 "minute" 2 "hour" 6 "day" 29 "month" 2 "year" 2020 "week_day" 5 "year_day" 59 "is_dst" false)) + (test:eq (datetime:asUTCDate 0) (dict "millisecond" 0 "second" 0 "minute" 0 "hour" 0 "day" 1 "month" 1 "year" 1970 "week_day" 3 "year_day" 0 "is_dst" false)) + (test:eq (datetime:asUTCDate -1) (dict "millisecond" 0 "second" 59 "minute" 59 "hour" 23 "day" 31 "month" 12 "year" 1969 "week_day" 2 "year_day" 364 "is_dst" false)) + # largest representable exact integers (2^53 - 1) using doubles) + (test:eq (datetime:asUTCDate 9007199254740991) (dict "millisecond" 0 "second" 31 "minute" 36 "hour" 7 "day" 12 "month" 11 "year" 285428751 "week_day" 0 "year_day" 315 "is_dst" false)) + (test:eq (datetime:asUTCDate -9007199254740991) (dict "millisecond" 0 "second" 29 "minute" 23 "hour" 16 "day" 20 "month" 2 "year" -285424812 "week_day" 5 "year_day" 50 "is_dst" false)) }) + + (test:case "plus/minus unit" { + (test:eq (datetime:second (datetime:plusSeconds t 2)) 6) + (test:eq (datetime:second (datetime:minusSeconds t 5)) 59) + (test:eq (datetime:minute (datetime:plusMinutes t 58)) 0) + (test:eq (datetime:minute (datetime:minusMinutes t 3)) 59) + (test:eq (datetime:hour (datetime:plusHours t 19)) 1) + (test:eq (datetime:hour (datetime:minusHours t 16)) 14) + (test:eq (datetime:day (datetime:plusDays t 19)) 5) + (test:eq (datetime:day (datetime:minusDays t 16)) 30) + (test:eq (datetime:day (datetime:plusWeeks t 3)) 7) + (test:eq (datetime:day (datetime:minusWeeks t 3)) 25) + (test:eq (datetime:month (datetime:plusMonths t 1)) 3) + (test:eq (datetime:month (datetime:minusMonths t 1)) 1) + (test:eq (datetime:year (datetime:plusYears t 1)) 2021) + (test:eq (datetime:year (datetime:minusYears t 1)) 2019) }) + + (test:case "atStartOfDay" { + (mut d (datetime:atStartOfDay now)) + (test:eq (datetime:hour d) 0) + (test:eq (datetime:minute d) 0) + (test:eq (datetime:second d) 0) + (test:eq (datetime:millisecond d) 0) + (test:eq (datetime:year d) (datetime:year now)) + (test:eq (datetime:month d) (datetime:month now)) + (test:eq (datetime:day d) (datetime:day now)) }) + + (test:case "atEndOfDay" { + (mut d (datetime:atEndOfDay now)) + (test:eq (datetime:hour d) 23) + (test:eq (datetime:minute d) 59) + (test:eq (datetime:second d) 59) + (test:eq (datetime:millisecond d) 999) + (test:eq (datetime:year d) (datetime:year now)) + (test:eq (datetime:month d) (datetime:month now)) + (test:eq (datetime:day d) (datetime:day now)) }) + + (test:case "today, yesterday, tomorrow" { + # returns the same value each day, the start of the day + (test:eq (datetime:today) (datetime:today)) + (test:eq (datetime:yesterday) (datetime:yesterday)) + (test:eq (datetime:tomorrow) (datetime:tomorrow)) + + (mut d (datetime:today)) + (test:eq (datetime:hour d) 0) + (test:eq (datetime:minute d) 0) + (test:eq (datetime:second d) 0) + (test:eq (datetime:year d) (datetime:year now)) + (test:eq (datetime:month d) (datetime:month now)) + (test:eq (datetime:day d) (datetime:day now)) + + (mut d (datetime:yesterday)) + (test:eq (datetime:hour d) 0) + (test:eq (datetime:minute d) 0) + (test:eq (datetime:second d) 0) + (test:eq (datetime:year d) (datetime:year now)) + (test:eq (datetime:month d) (datetime:month now)) + (test:eq (datetime:day d) (- (datetime:day now) 1)) + + (mut d (datetime:tomorrow)) + (test:eq (datetime:hour d) 0) + (test:eq (datetime:minute d) 0) + (test:eq (datetime:second d) 0) + (test:eq (datetime:year d) (datetime:year now)) + (test:eq (datetime:month d) (datetime:month now)) + (test:eq (datetime:day d) (+ (datetime:day now) 1)) }) + + (test:case "nextDay, previousDay" { + (mut d (datetime:nextDay t)) + (test:eq (datetime:hour d) (datetime:hour t)) + (test:eq (datetime:minute d) (datetime:minute t)) + (test:eq (datetime:second d) (datetime:second t)) + (test:eq (datetime:millisecond d) (datetime:millisecond t)) + (test:eq (datetime:year d) (datetime:year t)) + (test:eq (datetime:month d) (datetime:month t)) + (test:eq (datetime:day d) (+ 1 (datetime:day t))) + + (mut d (datetime:nextDay t0229)) + (test:eq (datetime:year d) 2020) + (test:eq (datetime:month d) 3) + (test:eq (datetime:day d) 1) + + (mut d (datetime:previousDay t0301)) + (test:eq (datetime:year d) 2020) + (test:eq (datetime:month d) 2) + (test:eq (datetime:day d) 29) + + (mut d (datetime:previousDay t)) + (test:eq (datetime:hour d) (datetime:hour t)) + (test:eq (datetime:minute d) (datetime:minute t)) + (test:eq (datetime:second d) (datetime:second t)) + (test:eq (datetime:millisecond d) (datetime:millisecond t)) + (test:eq (datetime:year d) (datetime:year t)) + (test:eq (datetime:month d) (datetime:month t)) + (test:eq (datetime:day d) (- (datetime:day t) 1)) }) + + (test:case "delta, asDelta" { + (test:eq (datetime:asSeconds (datetime:delta datetime:year1970 datetime:year1970)) 0) + (test:eq (datetime:asSeconds (datetime:delta datetime:year1970 datetime:year2000)) 946684800) + (test:eq (datetime:asDelta 60) oneMinuteAfterY0Delta) }) + + (test:case "asSeconds" { + (test:eq (datetime:asSeconds oneMinuteAfterY0) 60) + (test:eq (datetime:asSeconds (datetime:asUTCDate datetime:year0)) 0) + (test:eq (datetime:asSeconds (datetime:asUTCDate datetime:year1970)) 62167219200) }) + + (test:case "plusDelta, minusDelta" { + (mut delta (datetime:asDelta 90061)) + (mut d (datetime:plusDelta datetime:year1970 delta)) + (test:eq d 90061) + (test:eq (datetime:year d) 1970) + (test:eq (datetime:month d) 1) + (test:eq (datetime:day d) 2) + (test:eq (datetime:hour d) 1) + (test:eq (datetime:minute d) 1) + (test:eq (datetime:second d) 1) + + (test:eq (datetime:plusDelta datetime:year1970 oneMinuteAfterY0Delta) 60) + (test:eq (datetime:minusDelta datetime:year1970 oneMinuteAfterY0Delta) -60) }) + + (test:case "extract component from timestamp" { + (test:eq (datetime:year datetime:year1970) 1970) + (test:eq (datetime:month datetime:year1970) 1) + (test:eq (datetime:day datetime:year1970) 1) + (test:eq (datetime:hour datetime:year1970) 0) + (test:eq (datetime:minute datetime:year1970) 0) + (test:eq (datetime:second datetime:year1970) 0) + (test:eq (datetime:millisecond datetime:year1970) 0) + + (mut d (datetime:makeUTCTimestamp 2020 2 29 6 2 4 550 nil false)) + (test:eq (datetime:year d) 2020) + (test:eq (datetime:month d) 2) + (test:eq (datetime:day d) 29) + (test:eq (datetime:hour d) 6) + (test:eq (datetime:minute d) 2) + (test:eq (datetime:second d) 4) + (test:eq (datetime:millisecond d) 550) }) + + (test:case "leapYear?" { + (test:expect (datetime:leapYear? 2400)) + (test:expect (datetime:leapYear? 2004)) + (test:expect (datetime:leapYear? 2000)) + (test:expect (datetime:leapYear? 1992)) + (test:expect (datetime:leapYear? 1600)) + (test:expect (not (datetime:leapYear? 2600))) + (test:expect (not (datetime:leapYear? 2500))) + (test:expect (not (datetime:leapYear? 2300))) + (test:expect (not (datetime:leapYear? 2200))) + (test:expect (not (datetime:leapYear? 2100))) + (test:expect (not (datetime:leapYear? 1900))) + (test:expect (not (datetime:leapYear? 1800))) + (test:expect (not (datetime:leapYear? 1700))) }) + + (test:case "month/year length" { + (test:eq (datetime:monthLength (datetime:makeUTCTimestamp 2400 1 1 1 1 1 1 nil false)) 31) + (test:eq (datetime:monthLength (datetime:makeUTCTimestamp 2400 2 1 1 1 1 1 nil false)) 29) + (test:eq (datetime:monthLength (datetime:makeUTCTimestamp 2401 2 1 1 1 1 1 nil false)) 28) + (test:eq (datetime:monthLength (datetime:makeUTCTimestamp 2400 3 1 1 1 1 1 nil false)) 31) + (test:eq (datetime:monthLength (datetime:makeUTCTimestamp 2400 4 1 1 1 1 1 nil false)) 30) + (test:eq (datetime:monthLength (datetime:makeUTCTimestamp 2400 5 1 1 1 1 1 nil false)) 31) + (test:eq (datetime:monthLength (datetime:makeUTCTimestamp 2400 6 1 1 1 1 1 nil false)) 30) + (test:eq (datetime:monthLength (datetime:makeUTCTimestamp 2400 7 1 1 1 1 1 nil false)) 31) + (test:eq (datetime:monthLength (datetime:makeUTCTimestamp 2400 8 1 1 1 1 1 nil false)) 31) + (test:eq (datetime:monthLength (datetime:makeUTCTimestamp 2400 9 1 1 1 1 1 nil false)) 30) + (test:eq (datetime:monthLength (datetime:makeUTCTimestamp 2400 10 1 1 1 1 1 nil false)) 31) + (test:eq (datetime:monthLength (datetime:makeUTCTimestamp 2400 11 1 1 1 1 1 nil false)) 30) + (test:eq (datetime:monthLength (datetime:makeUTCTimestamp 2400 12 1 1 1 1 1 nil false)) 31) + + (test:eq (datetime:yearLength (datetime:makeUTCTimestamp 2400 12 1 1 1 1 1 nil false)) 366) + (test:eq (datetime:yearLength (datetime:makeUTCTimestamp 2000 12 1 1 1 1 1 nil false)) 366) + (test:eq (datetime:yearLength (datetime:makeUTCTimestamp 2004 12 1 1 1 1 1 nil false)) 366) + (test:eq (datetime:yearLength (datetime:makeUTCTimestamp 1992 12 1 1 1 1 1 nil false)) 366) + (test:eq (datetime:yearLength (datetime:makeUTCTimestamp 1600 12 1 1 1 1 1 nil false)) 366) + + (test:eq (datetime:yearLength (datetime:makeUTCTimestamp 2401 12 1 1 1 1 1 nil false)) 365) + (test:eq (datetime:yearLength (datetime:makeUTCTimestamp 2600 12 1 1 1 1 1 nil false)) 365) + (test:eq (datetime:yearLength (datetime:makeUTCTimestamp 2500 12 1 1 1 1 1 nil false)) 365) + (test:eq (datetime:yearLength (datetime:makeUTCTimestamp 2300 12 1 1 1 1 1 nil false)) 365) + (test:eq (datetime:yearLength (datetime:makeUTCTimestamp 2200 12 1 1 1 1 1 nil false)) 365) + (test:eq (datetime:yearLength (datetime:makeUTCTimestamp 2100 12 1 1 1 1 1 nil false)) 365) + (test:eq (datetime:yearLength (datetime:makeUTCTimestamp 2026 12 1 1 1 1 1 nil false)) 365) + (test:eq (datetime:yearLength (datetime:makeUTCTimestamp 1900 12 1 1 1 1 1 nil false)) 365) + (test:eq (datetime:yearLength (datetime:makeUTCTimestamp 1800 12 1 1 1 1 1 nil false)) 365) + (test:eq (datetime:yearLength (datetime:makeUTCTimestamp 1700 12 1 1 1 1 1 nil false)) 365) }) + + (test:case "utcOffsetRepr" { + (test:eq (datetime:utcOffsetRepr "Europe/Paris") "+01:00") + (test:eq (datetime:utcOffsetRepr "Antarctica/Palmer") "-03:00") + (test:eq (datetime:utcOffsetRepr "Asia/Kathmandu") "+05:45") }) + + (test:case "asISO8601" { + (test:eq (datetime:asISO8601 -1) "1969-12-31T23:59:59.000Z") + (test:eq (datetime:asISO8601 0) "1970-01-01T00:00:00.000Z") + (test:eq (datetime:asISO8601 1) "1970-01-01T00:00:01.000Z") }) + + (test:case "parse, parseAs" { + (test:eq (datetime:parse "1969-12-31T23:59:59") -1) + (test:eq (datetime:parse "1970-01-01T00:00:00") 0) + (test:eq (datetime:parse "2000-01-01T00:00:00") datetime:year2000) + (test:eq (datetime:parse "0000-01-01T00:00:00") datetime:year0) + + (test:eq (datetime:parseAs "1969-12-31" "%Y-%m-%d") -86400) + (test:eq (datetime:parseAs "1970-01-01" "%Y-%m-%d") 0) + (test:eq (datetime:parseAs "2000-01-01" "%Y-%m-%d") datetime:year2000) + (test:eq (datetime:parseAs "0000-01-01" "%Y-%m-%d") datetime:year0) }) }) diff --git a/tests/dict-tests.ark b/tests/dict-tests.ark index 8978ae6..bb043d0 100644 --- a/tests/dict-tests.ark +++ b/tests/dict-tests.ark @@ -81,6 +81,11 @@ (test:eq (dict:entries d) [["key" "value"] [5 12] [true true] [false false] [foo "yes"] [closure foo]]) (test:eq (dict:entries empty) []) }) + (test:case "fromList" { + (test:expect (empty? (dict:fromList []))) + (test:eq (dict:fromList (dict:entries d)) d) + (test:eq (dict:fromList [["a" 1]]) (dict "a" 1)) }) + (test:case "add" { (test:eq (dict:get d "test") nil) (dict:add d "test" 5) diff --git a/tests/list-tests.ark b/tests/list-tests.ark index 460c0f3..13ed408 100644 --- a/tests/list-tests.ark +++ b/tests/list-tests.ark @@ -32,6 +32,15 @@ (test:eq (list:slice1 [1 2 3 4 5] 0 4) [1 2 3 4]) (test:eq (list:slice1 [1 2 3 4 5] 0 1) [1]) }) + (test:case "sorted?" { + (test:expect (list:sorted? a)) + (test:expect (list:sorted? b)) + (test:expect (list:sorted? [])) + (test:expect (list:sorted? [1])) + (test:expect (not (list:sorted? [3 2 1]))) + (test:expect (not (list:sorted? [1 3 2]))) + (test:expect (list:sorted? [1 1])) }) + (test:case "zeros, ones" { (test:eq (list:zeros 0) []) (test:eq (list:zeros 1) [0]) @@ -41,6 +50,62 @@ (test:eq (list:ones 1) [1]) (test:eq (list:ones 5) [1 1 1 1 1]) }) + (test:case "findAfter" { + (test:eq (list:findAfter a 1 1) -1) + (test:eq (list:findAfter a 1 0) 0) + (test:eq (list:findAfter a 2 1) 1) + (test:eq (list:findAfter [1 2 3 1] 1 1) 3) + (test:eq (list:findAfter [] 0 1) -1) }) + + (test:case "findIf" { + (test:eq (list:findIf [] even?) nil) + (test:eq (list:findIf [1] even?) nil) + (test:eq (list:findIf [1 3 5] even?) nil) + (test:eq (list:findIf [1 2 3] even?) 1) + (test:eq (list:findIf [1 2 3 4] even?) 1) }) + + (test:case "search" { + (test:eq (list:search [1 2 3 4 5 6] [1 2 3 4 5 6]) 0) + (test:eq (list:search [1 2 3 4 5 6] [1 2 3 4]) 0) + (test:eq (list:search [1 2 3 4 5 6] [1 2 3]) 0) + (test:eq (list:search [1 2 3 4 5 6] [2 3 4]) 1) + (test:eq (list:search [1 2 3 4 5 6] [4 5 6]) 3) + (test:eq (list:search [1 2 3 4 5 6] [6]) 5) + (test:eq (list:search [1 2 3 4 5 6] [2 4]) nil) + (test:eq (list:search [1 2 3 4 5 6] [2]) 1) + (test:eq (list:search [] [2]) nil) + (test:eq (list:search [1 1 1 1 1 1] [1]) 0) + (test:eq (list:search [1 1 1 1 1 1] [1 1]) 0) }) + + (test:case "lowerBound, upperBound, binarySearch" { + (test:eq (list:lowerBound [1 2 4 5 5 6] 0) 0) + (test:eq (list:lowerBound [1 2 4 5 5 6] 1) 0) + (test:eq (list:lowerBound [1 2 4 5 5 6] 2) 1) + (test:eq (list:lowerBound [1 2 4 5 5 6] 3) 2) + (test:eq (list:lowerBound [1 2 4 5 5 6] 4) 2) + (test:eq (list:lowerBound [1 2 4 5 5 6] 5) 3) + (test:eq (list:lowerBound [1 2 4 5 5 6] 6) 5) + (test:eq (list:lowerBound [1 2 4 5 5 6] 7) nil) + (test:eq (list:lowerBound [] 1) nil) + (test:eq (list:lowerBound [100.0 101.5 102.5 102.5 107.3] 102.5) 2) + (test:eq (list:lowerBound [100.0 101.5 102.5 102.5 107.3] 110) nil) + + (test:eq (list:upperBound [1 2 4 5 5 6] 0) 0) + (test:eq (list:upperBound [1 2 4 5 5 6] 1) 1) + (test:eq (list:upperBound [1 2 4 5 5 6] 2) 2) + (test:eq (list:upperBound [1 2 4 5 5 6] 3) 2) + (test:eq (list:upperBound [1 2 4 5 5 6] 4) 3) + (test:eq (list:upperBound [1 2 4 5 5 6] 5) 5) + (test:eq (list:upperBound [1 2 4 5 5 6] 6) nil) + (test:eq (list:upperBound [] 1) nil) + (test:eq (list:upperBound [100.0 101.5 102.5 102.5 107.3] 102.5) 4) + (test:eq (list:upperBound [100.0 101.5 102.5 102.5 107.3] 110) nil) + + (test:eq (list:binarySearch [1 3 4 5 9] 1) true) + (test:eq (list:binarySearch [1 3 4 5 9] 2) false) + (test:eq (list:binarySearch [1 3 4 5 9] 3) true) + (test:eq (list:binarySearch [] 3) false) }) + (test:case "contains?" { (test:expect (list:contains? [1 2 3] 1)) (test:expect (not (list:contains? [1 2 3] "1"))) @@ -51,9 +116,12 @@ (test:eq (len [1 2 3]) 3) }) (test:case "forEach" { + (mut count 0) (list:forEach a (fun (e) { + (set count (+ 1 count)) # just assert we have something, basically it's just a while + @ - (test:neq e nil) })) }) + (test:neq e nil) })) + (test:eq count (len a)) }) (test:case "enumerate" { (mut position 0) @@ -61,7 +129,8 @@ (test:eq idx position) (set position (+ 1 position)) # just assert we have something, basically it's just a while + @ - (test:eq (type e) "Number") })) }) + (test:eq (type e) "Number") })) + (test:eq position (len a)) }) (test:case "product" { (test:eq (list:product b) (* 4 5 6)) @@ -96,6 +165,12 @@ (test:eq (list:max b) 6) (test:eq (list:max [-1]) -1) }) + (test:case "minMax" { + (test:eq (list:minMax []) [nil nil]) + (test:eq (list:minMax [1]) [1 1]) + (test:eq (list:minMax b) [4 6]) + (test:eq (list:minMax [-1]) [-1 -1]) }) + (test:case "median" { (test:eq (list:median []) nil) (test:eq (list:median [1]) 1) @@ -114,6 +189,27 @@ (test:eq (list:dropWhile a (fun (c) (< c 2))) [2 3]) (test:eq (list:dropWhile a (fun (c) (< c 5))) []) }) + (test:case "shiftLeft, shiftRight" { + (test:eq (list:shiftLeft [] 0) []) + (test:eq (list:shiftLeft [1] 0) [1]) + (test:eq (list:shiftLeft [1 2] 0) [1 2]) + (test:eq (list:shiftLeft [] 1) []) + (test:eq (list:shiftLeft [1] 1) [1]) + (test:eq (list:shiftLeft [1 2] 1) [2 1]) + (test:eq (list:shiftLeft [1 2 3 4 5] 2) [3 4 5 1 2]) + (test:eq (list:shiftLeft [1 2 3 4 5] 5) [1 2 3 4 5]) + (test:eq (list:shiftLeft [1 2 3 4 5] 11) [2 3 4 5 1]) + + (test:eq (list:shiftRight [] 0) []) + (test:eq (list:shiftRight [1] 0) [1]) + (test:eq (list:shiftRight [1 2] 0) [1 2]) + (test:eq (list:shiftRight [] 1) []) + (test:eq (list:shiftRight [1] 1) [1]) + (test:eq (list:shiftRight [1 2] 1) [2 1]) + (test:eq (list:shiftRight [1 2 3 4 5] 2) [4 5 1 2 3]) + (test:eq (list:shiftRight [1 2 3 4 5] 5) [1 2 3 4 5]) + (test:eq (list:shiftRight [1 2 3 4 5] 11) [5 1 2 3 4]) }) + (test:case "filter" { (test:eq (list:filter a math:even?) [2]) (test:eq (list:filter a (fun (e) (> e 100))) []) diff --git a/tests/macros-tests.ark b/tests/macros-tests.ark index 3c130d2..d8e029e 100644 --- a/tests/macros-tests.ark +++ b/tests/macros-tests.ark @@ -51,6 +51,16 @@ (until true (test:expect false "this shouldn't trigger")) }) + (test:case "unpack" { + (unpack [1 2 3] a b c) + (test:eq a 1) + (test:eq b 2) + (test:eq c 3) + + (unpack [4 5 6] d e) + (test:eq d 4) + (test:eq e 5) }) + (test:case "++ and --" { (mut i 0) (test:eq (++ i) 1)