| 1 | module time |
| 2 | |
| 3 | pub const days_string = 'MonTueWedThuFriSatSun' |
| 4 | pub const long_days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']! |
| 5 | pub const month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]! |
| 6 | pub const months_string = 'JanFebMarAprMayJunJulAugSepOctNovDec' |
| 7 | pub const long_months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', |
| 8 | 'September', 'October', 'November', 'December'] |
| 9 | // The unsigned zero year for internal calculations. |
| 10 | // Must be 1 mod 400, and times before it will not compute correctly, |
| 11 | // but otherwise can be changed at will. |
| 12 | pub const absolute_zero_year = i64(-292277022399) |
| 13 | pub const seconds_per_minute = 60 |
| 14 | pub const seconds_per_hour = 60 * seconds_per_minute |
| 15 | pub const seconds_per_day = 24 * seconds_per_hour |
| 16 | pub const seconds_per_week = 7 * seconds_per_day |
| 17 | pub const days_per_400_years = days_in_year * 400 + 97 |
| 18 | pub const days_per_100_years = days_in_year * 100 + 24 |
| 19 | pub const days_per_4_years = days_in_year * 4 + 1 |
| 20 | pub const days_in_year = 365 |
| 21 | pub const days_before = [ |
| 22 | 0, |
| 23 | 31, |
| 24 | 31 + 28, |
| 25 | 31 + 28 + 31, |
| 26 | 31 + 28 + 31 + 30, |
| 27 | 31 + 28 + 31 + 30 + 31, |
| 28 | 31 + 28 + 31 + 30 + 31 + 30, |
| 29 | 31 + 28 + 31 + 30 + 31 + 30 + 31, |
| 30 | 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31, |
| 31 | 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30, |
| 32 | 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31, |
| 33 | 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30, |
| 34 | 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31, |
| 35 | ]! |
| 36 | |
| 37 | // Time contains various time units for a point in time. |
| 38 | @[markused] |
| 39 | pub struct Time { |
| 40 | unix i64 |
| 41 | pub: |
| 42 | year int |
| 43 | month int |
| 44 | day int |
| 45 | hour int |
| 46 | minute int |
| 47 | second int |
| 48 | nanosecond int |
| 49 | is_local bool // used to make time.now().local().local() == time.now().local() |
| 50 | } |
| 51 | |
| 52 | // FormatDelimiter contains different time formats. |
| 53 | pub enum FormatTime { |
| 54 | hhmm12 |
| 55 | hhmm24 |
| 56 | hhmmss12 |
| 57 | hhmmss24 |
| 58 | hhmmss24_milli |
| 59 | hhmmss24_micro |
| 60 | hhmmss24_nano |
| 61 | no_time |
| 62 | } |
| 63 | |
| 64 | // FormatDelimiter contains different date formats. |
| 65 | pub enum FormatDate { |
| 66 | ddmmyy |
| 67 | ddmmyyyy |
| 68 | mmddyy |
| 69 | mmddyyyy |
| 70 | mmmd |
| 71 | mmmdd |
| 72 | mmmddyy |
| 73 | mmmddyyyy |
| 74 | no_date |
| 75 | yyyymmdd |
| 76 | yymmdd |
| 77 | } |
| 78 | |
| 79 | // FormatDelimiter contains different time/date delimiters. |
| 80 | pub enum FormatDelimiter { |
| 81 | dot |
| 82 | hyphen |
| 83 | slash |
| 84 | space |
| 85 | no_delimiter |
| 86 | } |
| 87 | |
| 88 | fn normalize_new_time(t Time) Time { |
| 89 | month := if t.month == 0 { 1 } else { t.month } |
| 90 | day := if t.day == 0 { 1 } else { t.day } |
| 91 | if t.year < -9999 || t.year > 9999 { |
| 92 | panic('invalid time: year must be between -9999 and 9999') |
| 93 | } |
| 94 | if month < 1 || month > 12 { |
| 95 | panic('invalid time: month must be between 1 and 12') |
| 96 | } |
| 97 | max_day := month_days[month - 1] + if month == 2 && is_leap_year(t.year) { |
| 98 | 1 |
| 99 | } else { |
| 100 | 0 |
| 101 | } |
| 102 | if day < 1 || day > max_day { |
| 103 | panic('invalid time: day must be between 1 and ${max_day} for year ${t.year}, month ${month}') |
| 104 | } |
| 105 | if t.hour < 0 || t.hour > 23 { |
| 106 | panic('invalid time: hour must be between 0 and 23') |
| 107 | } |
| 108 | if t.minute < 0 || t.minute > 59 { |
| 109 | panic('invalid time: minute must be between 0 and 59') |
| 110 | } |
| 111 | if t.second < 0 || t.second > 59 { |
| 112 | panic('invalid time: second must be between 0 and 59') |
| 113 | } |
| 114 | if t.nanosecond < 0 || t.nanosecond >= 1_000_000_000 { |
| 115 | panic('invalid time: nanosecond must be between 0 and 999999999') |
| 116 | } |
| 117 | return Time{ |
| 118 | ...t |
| 119 | month: month |
| 120 | day: day |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | // Time.new returns a time struct with the calculated Unix time. |
| 125 | pub fn Time.new(t Time) Time { |
| 126 | return time_with_unix(normalize_new_time(t)) |
| 127 | } |
| 128 | |
| 129 | // new returns a time struct with the calculated Unix time. |
| 130 | pub fn new(t Time) Time { |
| 131 | return Time.new(t) |
| 132 | } |
| 133 | |
| 134 | // smonth returns the month name abbreviation. |
| 135 | pub fn (t Time) smonth() string { |
| 136 | if t.month <= 0 || t.month > 12 { |
| 137 | return '---' |
| 138 | } |
| 139 | i := t.month - 1 |
| 140 | return months_string[i * 3..(i + 1) * 3] |
| 141 | } |
| 142 | |
| 143 | // unix returns the UNIX time with second resolution. |
| 144 | @[inline] |
| 145 | pub fn (t Time) unix() i64 { |
| 146 | return time_with_unix(t.local_to_utc()).unix |
| 147 | } |
| 148 | |
| 149 | // local_unix returns the UNIX local time with second resolution. |
| 150 | @[inline] |
| 151 | pub fn (t Time) local_unix() i64 { |
| 152 | return time_with_unix(t).unix |
| 153 | } |
| 154 | |
| 155 | // is_zero returns true when `t` is the zero value of `time.Time`. |
| 156 | @[inline] |
| 157 | pub fn (t Time) is_zero() bool { |
| 158 | return t.unix == 0 && t.year == 0 && t.month == 0 && t.day == 0 && t.hour == 0 && t.minute == 0 |
| 159 | && t.second == 0 && t.nanosecond == 0 && !t.is_local |
| 160 | } |
| 161 | |
| 162 | // unix_milli returns the UNIX time with millisecond resolution. |
| 163 | @[inline] |
| 164 | pub fn (t Time) unix_milli() i64 { |
| 165 | return t.unix() * 1_000 + (i64(t.nanosecond) / 1_000_000) |
| 166 | } |
| 167 | |
| 168 | // unix_micro returns the UNIX time with microsecond resolution. |
| 169 | @[inline] |
| 170 | pub fn (t Time) unix_micro() i64 { |
| 171 | return t.unix() * 1_000_000 + (i64(t.nanosecond) / 1_000) |
| 172 | } |
| 173 | |
| 174 | // unix_nano returns the UNIX time with nanosecond resolution. |
| 175 | @[inline] |
| 176 | pub fn (t Time) unix_nano() i64 { |
| 177 | // TODO: use i128 here, when V supports it, since the following expression overflows for years like 3001: |
| 178 | return t.unix() * 1_000_000_000 + i64(t.nanosecond) |
| 179 | } |
| 180 | |
| 181 | // add returns a new time with the given duration added. |
| 182 | pub fn (t Time) add(duration_in_nanosecond Duration) Time { |
| 183 | // This expression overflows i64 for big years (and we do not have i128 yet): |
| 184 | // nanos := t.unix * 1_000_000_000 + i64(t.nanosecond) <- |
| 185 | // ... so instead, handle the addition manually in parts ¯\_(ツ)_/¯ |
| 186 | mut increased_time_nanosecond := i64(t.nanosecond) + duration_in_nanosecond.nanoseconds() |
| 187 | // increased_time_second |
| 188 | mut increased_time_second := t.local_unix() + (increased_time_nanosecond / second) |
| 189 | increased_time_nanosecond = increased_time_nanosecond % second |
| 190 | if increased_time_nanosecond < 0 { |
| 191 | increased_time_second-- |
| 192 | increased_time_nanosecond += second |
| 193 | } |
| 194 | res := unix_nanosecond(increased_time_second, int(increased_time_nanosecond)) |
| 195 | |
| 196 | if t.is_local { |
| 197 | // we need to reset unix to 0, because we don't know the offset |
| 198 | // and we can't calculate it without it without causing infinite recursion |
| 199 | // so unfortunately we need to recalculate unix next time it is needed |
| 200 | return Time{ |
| 201 | ...res |
| 202 | is_local: true |
| 203 | unix: 0 |
| 204 | } |
| 205 | } |
| 206 | |
| 207 | return res |
| 208 | } |
| 209 | |
| 210 | // add_seconds returns a new time struct with an added number of seconds. |
| 211 | pub fn (t Time) add_seconds(seconds int) Time { |
| 212 | return time_with_unix(t).add(i64(seconds) * second) |
| 213 | } |
| 214 | |
| 215 | // add_days returns a new time struct with an added number of days. |
| 216 | pub fn (t Time) add_days(days int) Time { |
| 217 | return time_with_unix(t).add(i64(days) * 24 * hour) |
| 218 | } |
| 219 | |
| 220 | // since returns the time duration elapsed since a given time. |
| 221 | pub fn since(t Time) Duration { |
| 222 | return now() - t |
| 223 | } |
| 224 | |
| 225 | // relative returns a string representation of the difference between t |
| 226 | // and the current time. |
| 227 | // |
| 228 | // Sample outputs: |
| 229 | // ``` |
| 230 | // // future |
| 231 | // now |
| 232 | // in 5 minutes |
| 233 | // in 1 day |
| 234 | // on Feb 17 |
| 235 | // // past |
| 236 | // 2 hours ago |
| 237 | // last Jan 15 |
| 238 | // 5 years ago |
| 239 | // ``` |
| 240 | pub fn (t Time) relative() string { |
| 241 | znow := now() |
| 242 | mut secs := znow.unix() - t.unix() |
| 243 | mut prefix := '' |
| 244 | mut suffix := '' |
| 245 | if secs < 0 { |
| 246 | secs *= -1 |
| 247 | prefix = 'in ' |
| 248 | } else { |
| 249 | suffix = ' ago' |
| 250 | } |
| 251 | if secs < seconds_per_minute / 2 { |
| 252 | return 'now' |
| 253 | } |
| 254 | if secs < seconds_per_hour { |
| 255 | m := secs / seconds_per_minute |
| 256 | if m == 1 { |
| 257 | return '${prefix}1 minute${suffix}' |
| 258 | } |
| 259 | return '${prefix}${m} minutes${suffix}' |
| 260 | } |
| 261 | if secs < seconds_per_hour * 24 { |
| 262 | h := secs / seconds_per_hour |
| 263 | if h == 1 { |
| 264 | return '${prefix}1 hour${suffix}' |
| 265 | } |
| 266 | return '${prefix}${h} hours${suffix}' |
| 267 | } |
| 268 | if secs < seconds_per_hour * 24 * 7 { |
| 269 | d := secs / seconds_per_hour / 24 |
| 270 | if d == 1 { |
| 271 | return '${prefix}1 day${suffix}' |
| 272 | } |
| 273 | return '${prefix}${d} days${suffix}' |
| 274 | } |
| 275 | if secs < seconds_per_hour * 24 * days_in_year { |
| 276 | if prefix == 'in ' { |
| 277 | return 'on ${t.md()}' |
| 278 | } |
| 279 | return 'last ${t.md()}' |
| 280 | } |
| 281 | y := secs / seconds_per_hour / 24 / days_in_year |
| 282 | if y == 1 { |
| 283 | return '${prefix}1 year${suffix}' |
| 284 | } |
| 285 | return '${prefix}${y} years${suffix}' |
| 286 | } |
| 287 | |
| 288 | // relative_short returns a string saying how long ago a time occurred as follows: |
| 289 | // 0-30 seconds: `"now"`; 30-60 seconds: `"1m"`; anything else is rounded to the |
| 290 | // nearest minute, hour, day, or year |
| 291 | // |
| 292 | // Sample outputs: |
| 293 | // ``` |
| 294 | // // future |
| 295 | // now |
| 296 | // in 5m |
| 297 | // in 1d |
| 298 | // // past |
| 299 | // 2h ago |
| 300 | // 5y ago |
| 301 | // ``` |
| 302 | pub fn (t Time) relative_short() string { |
| 303 | znow := now() |
| 304 | mut secs := znow.unix() - t.unix() |
| 305 | mut prefix := '' |
| 306 | mut suffix := '' |
| 307 | if secs < 0 { |
| 308 | secs *= -1 |
| 309 | prefix = 'in ' |
| 310 | } else { |
| 311 | suffix = ' ago' |
| 312 | } |
| 313 | if secs < seconds_per_minute / 2 { |
| 314 | return 'now' |
| 315 | } |
| 316 | if secs < seconds_per_hour { |
| 317 | m := secs / seconds_per_minute |
| 318 | if m == 1 { |
| 319 | return '${prefix}1m${suffix}' |
| 320 | } |
| 321 | return '${prefix}${m}m${suffix}' |
| 322 | } |
| 323 | if secs < seconds_per_hour * 24 { |
| 324 | h := secs / seconds_per_hour |
| 325 | if h == 1 { |
| 326 | return '${prefix}1h${suffix}' |
| 327 | } |
| 328 | return '${prefix}${h}h${suffix}' |
| 329 | } |
| 330 | if secs < seconds_per_hour * 24 * days_in_year { |
| 331 | d := secs / seconds_per_hour / 24 |
| 332 | if d == 1 { |
| 333 | return '${prefix}1d${suffix}' |
| 334 | } |
| 335 | return '${prefix}${d}d${suffix}' |
| 336 | } |
| 337 | y := secs / seconds_per_hour / 24 / days_in_year |
| 338 | if y == 1 { |
| 339 | return '${prefix}1y${suffix}' |
| 340 | } |
| 341 | return '${prefix}${y}y${suffix}' |
| 342 | } |
| 343 | |
| 344 | // day_of_week returns the current day of a given year, month, and day, as an integer. |
| 345 | pub fn day_of_week(y int, m int, d int) int { |
| 346 | // Sakomotho's algorithm is explained here: |
| 347 | // https://stackoverflow.com/a/6385934 |
| 348 | t := [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4] |
| 349 | mut sy := y |
| 350 | if m < 3 { |
| 351 | sy = sy - 1 |
| 352 | } |
| 353 | return (sy + sy / 4 - sy / 100 + sy / 400 + t[iclamp(0, m - 1, 11)] + d - 1) % 7 + 1 |
| 354 | } |
| 355 | |
| 356 | // day_of_week returns the current day as an integer. |
| 357 | pub fn (t Time) day_of_week() int { |
| 358 | return day_of_week(t.year, t.month, t.day) |
| 359 | } |
| 360 | |
| 361 | // week_of_year returns the current week of year as an integer. |
| 362 | // follow ISO 8601 standard |
| 363 | pub fn (t Time) week_of_year() int { |
| 364 | // ISO 8601 Week of Year Rules: |
| 365 | // -------------------------------------------- |
| 366 | // 1. Week Definition: |
| 367 | // - A week starts on **Monday** (Day 1) and ends on **Sunday** (Day 7). |
| 368 | // 2. First Week of the Year: |
| 369 | // - The first week is the one containing the year's **first Thursday**. |
| 370 | // - Equivalently, the week with January 4th always belongs to Week 1. |
| 371 | // 3. Year Assignment: |
| 372 | // - Dates in December/January may belong to the previous/next ISO year, |
| 373 | // depending on the week's Thursday. |
| 374 | // 4. Week Number Format: |
| 375 | // - Expressed as `YYYY-Www` (e.g., `2026-W01` for the first week of 2026). |
| 376 | // -------------------------------------------- |
| 377 | // Algorithm Steps: |
| 378 | // 1. Find the Thursday of the current week: |
| 379 | // - If date is Monday-Wednesday, add days to reach Thursday. |
| 380 | // - If date is Thursday-Sunday, subtract days to reach Thursday. |
| 381 | // 2. The ISO year is the calendar year of this Thursday. |
| 382 | // 3. Compute the week number as: |
| 383 | // week_number = (thursday's day_of_year - 1) / 7 + 1 |
| 384 | dow := t.day_of_week() |
| 385 | days_to_thursday := 4 - dow |
| 386 | thursday_date := t.add_days(days_to_thursday) |
| 387 | thursday_day_of_year := thursday_date.year_day() |
| 388 | week_number := (thursday_day_of_year - 1) / 7 + 1 |
| 389 | return week_number |
| 390 | } |
| 391 | |
| 392 | // year_day returns the current day of the year as an integer. |
| 393 | // See also #Time.custom_format . |
| 394 | pub fn (t Time) year_day() int { |
| 395 | yday := t.day + days_before[iclamp(0, t.month - 1, 12)] |
| 396 | if is_leap_year(t.year) && t.month > 2 { |
| 397 | return yday + 1 |
| 398 | } |
| 399 | return yday |
| 400 | } |
| 401 | |
| 402 | // weekday_str returns the current day as a string 3 letter abbreviation. |
| 403 | pub fn (t Time) weekday_str() string { |
| 404 | i := t.day_of_week() - 1 |
| 405 | return long_days[iclamp(0, i, 6)][0..3] |
| 406 | } |
| 407 | |
| 408 | // long_weekday_str returns the current day as a string. |
| 409 | pub fn (t Time) long_weekday_str() string { |
| 410 | i := t.day_of_week() - 1 |
| 411 | return long_days[iclamp(0, i, 6)] |
| 412 | } |
| 413 | |
| 414 | // is_leap_year checks if a given a year is a leap year. |
| 415 | pub fn is_leap_year(year int) bool { |
| 416 | return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) |
| 417 | } |
| 418 | |
| 419 | // days_in_month returns a number of days in a given month. |
| 420 | pub fn days_in_month(month int, year int) !int { |
| 421 | if month > 12 || month < 1 { |
| 422 | return error('Invalid month: ${month}') |
| 423 | } |
| 424 | extra := if month == 2 && is_leap_year(year) { 1 } else { 0 } |
| 425 | res := month_days[month - 1] + extra |
| 426 | return res |
| 427 | } |
| 428 | |
| 429 | // debug returns detailed breakdown of time (`Time{ year: YYYY month: MM day: dd hour: HH: minute: mm second: ss nanosecond: nanos unix: unix is_local: false }`). |
| 430 | pub fn (t Time) debug() string { |
| 431 | return 'Time{ year: ${t.year:04} month: ${t.month:02} day: ${t.day:02} hour: ${t.hour:02} minute: ${t.minute:02} second: ${t.second:02} nanosecond: ${t.nanosecond:09} unix: ${t.unix:07} is_local: ${t.is_local} }' |
| 432 | } |
| 433 | |
| 434 | // offset returns time zone UTC offset in seconds. |
| 435 | pub fn offset() int { |
| 436 | t := utc() |
| 437 | local := t.local() |
| 438 | return int(local.unix - t.unix) |
| 439 | } |
| 440 | |
| 441 | // local_to_utc converts the receiver `t` to the corresponding UTC time, if it contains local time. |
| 442 | // If the receiver already does contain UTC time, it returns it unchanged. |
| 443 | pub fn (t Time) local_to_utc() Time { |
| 444 | if !t.is_local { |
| 445 | return t |
| 446 | } |
| 447 | return Time{ |
| 448 | ...t.add(i64(-offset()) * second) |
| 449 | is_local: false |
| 450 | } |
| 451 | } |
| 452 | |
| 453 | // utc_to_local converts the receiver `u` to the corresponding local time, if it contains UTC time. |
| 454 | // If the receiver already does contain local time, it returns it unchanged. |
| 455 | pub fn (u Time) utc_to_local() Time { |
| 456 | if u.is_local { |
| 457 | return u |
| 458 | } |
| 459 | return Time{ |
| 460 | ...u.add(i64(offset()) * second) |
| 461 | is_local: true |
| 462 | } |
| 463 | } |
| 464 | |
| 465 | // as_local returns the exact same time, as the receiver `t`, but with its .is_local field set to true. |
| 466 | // See also #Time.utc_to_local . |
| 467 | pub fn (t Time) as_local() Time { |
| 468 | return Time{ |
| 469 | ...t |
| 470 | is_local: true |
| 471 | } |
| 472 | } |
| 473 | |
| 474 | // as_utc returns the exact same time, as the receiver `t`, but with its .is_local field set to false. |
| 475 | // See also #Time.local_to_utc . |
| 476 | pub fn (t Time) as_utc() Time { |
| 477 | return Time{ |
| 478 | ...t |
| 479 | is_local: false |
| 480 | } |
| 481 | } |
| 482 | |
| 483 | // is_utc returns true, when the receiver `t` is a UTC time, and false otherwise. |
| 484 | // See also #Time.utc_to_local . |
| 485 | pub fn (t Time) is_utc() bool { |
| 486 | return !t.is_local |
| 487 | } |
| 488 | |