// A port of the parts of Python's `datetime` module used by the executor.
///|
pub(all) struct PyDate {
year : Int
month : Int
day : Int
} derive(Eq)
///|
pub(all) struct PyTime {
hour : Int
minute : Int
second : Int
microsecond : Int
} derive(Eq)
///|
/// A `datetime.datetime`; `tz` is the UTC offset in seconds of an aware datetime (only
/// `datetime.timezone.utc`, i.e. 0, is produced by the executor).
pub(all) struct PyDateTime {
date : PyDate
time : PyTime
tz : Int?
}
///|
/// A normalized `datetime.timedelta`.
pub(all) struct PyTimeDelta {
days : Int64
seconds : Int
microseconds : Int
} derive(Eq)
///|
let days_in_month_table : FixedArray[Int] = [
0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
]
///|
fn is_leap(y : Int) -> Bool {
y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)
}
///|
fn days_in_month(y : Int, m : Int) -> Int {
if m == 2 && is_leap(y) {
29
} else {
days_in_month_table[m]
}
}
///|
/// Python `date.toordinal()`.
fn ordinal(y : Int, m : Int, d : Int) -> Int64 {
let y1 = (y - 1).to_int64()
let mut days = y1 * 365L + y1 / 4L - y1 / 100L + y1 / 400L
for i in 1.. PyDate raise PyException {
if n < 1L || n > 3652059L {
raise PyException("OverflowError", "date value out of range")
}
// days since 0001-01-01
let mut n = n - 1L
let n400 = n / 146097L
n = n % 146097L
let n100 = n / 36524L
n = n % 36524L
let n4 = n / 1461L
n = n % 1461L
let n1 = n / 365L
n = n % 365L
let mut year = (n400 * 400L + n100 * 100L + n4 * 4L + n1 + 1L).to_int()
if n1 == 4L || n100 == 4L {
year -= 1
return { year, month: 12, day: 31, }
}
let mut month = 1
let mut rem = n.to_int()
while rem >= days_in_month(year, month) {
rem -= days_in_month(year, month)
month += 1
}
{ year, month, day: rem + 1, }
}
///|
fn check_date(y : Int, m : Int, d : Int) -> PyDate raise PyException {
if y < 1 || y > 9999 {
raise value_error("year \{y} is out of range")
}
if m < 1 || m > 12 {
raise value_error("month must be in 1..12")
}
if d < 1 || d > days_in_month(y, m) {
raise value_error("day is out of range for month")
}
{ year: y, month: m, day: d, }
}
///|
fn check_time(h : Int, mi : Int, s : Int, us : Int) -> PyTime raise PyException {
if h < 0 || h > 23 {
raise value_error("hour must be in 0..23")
}
if mi < 0 || mi > 59 {
raise value_error("minute must be in 0..59")
}
if s < 0 || s > 59 {
raise value_error("second must be in 0..59")
}
if us < 0 || us > 999999 {
raise value_error("microsecond must be in 0..999999")
}
{ hour: h, minute: mi, second: s, microsecond: us, }
}
///|
pub fn PyDate::toordinal(self : PyDate) -> Int64 {
ordinal(self.year, self.month, self.day)
}
///|
/// Python `date.weekday()` (Monday is 0).
pub fn PyDate::weekday(self : PyDate) -> Int {
((self.toordinal() + 6L) % 7L).to_int()
}
///|
fn PyDate::compare(self : PyDate, other : PyDate) -> Int {
self.toordinal().compare(other.toordinal())
}
///|
fn PyTime::compare(self : PyTime, other : PyTime) -> Int {
self.total_us().compare(other.total_us())
}
///|
fn PyTime::total_us(self : PyTime) -> Int64 {
((self.hour * 60 + self.minute) * 60 + self.second).to_int64() * 1000000L +
self.microsecond.to_int64()
}
///|
/// Microseconds since 0001-01-01 00:00 (naive, local).
fn PyDateTime::total_us(self : PyDateTime) -> Int64 {
(self.date.toordinal() - 1L) * 86400000000L + self.time.total_us()
}
///|
fn PyDateTime::utc_us(self : PyDateTime) -> Int64 {
self.total_us() - self.tz.unwrap_or(0).to_int64() * 1000000L
}
///|
fn PyDateTime::eq(self : PyDateTime, other : PyDateTime) -> Bool {
match (self.tz, other.tz) {
(None, None) => self.total_us() == other.total_us()
(Some(_), Some(_)) => self.utc_us() == other.utc_us()
_ => false
}
}
///|
fn PyDateTime::compare(
self : PyDateTime,
other : PyDateTime,
) -> Int raise PyException {
match (self.tz, other.tz) {
(None, None) => self.total_us().compare(other.total_us())
(Some(_), Some(_)) => self.utc_us().compare(other.utc_us())
_ =>
raise type_error("can't compare offset-naive and offset-aware datetimes")
}
}
///|
fn datetime_from_us(us : Int64, tz : Int?) -> PyDateTime raise PyException {
let day = floor_div64(us, 86400000000L)
let rem = us - day * 86400000000L
let date = from_ordinal(day + 1L)
let secs = (rem / 1000000L).to_int()
let micro = (rem % 1000000L).to_int()
{
date,
time: {
hour: secs / 3600,
minute: secs / 60 % 60,
second: secs % 60,
microsecond: micro,
},
tz,
}
}
///|
fn floor_div64(a : Int64, b : Int64) -> Int64 {
let q = a / b
if a % b != 0L && (a < 0L) != (b < 0L) {
q - 1L
} else {
q
}
}
///|
/// Builds a normalized timedelta from a total number of microseconds.
pub fn PyTimeDelta::from_us(us : Int64) -> PyTimeDelta {
let days = floor_div64(us, 86400000000L)
let rem = us - days * 86400000000L
{
days,
seconds: (rem / 1000000L).to_int(),
microseconds: (rem % 1000000L).to_int(),
}
}
///|
pub fn PyTimeDelta::total_us(self : PyTimeDelta) -> Int64 {
self.days * 86400000000L +
self.seconds.to_int64() * 1000000L +
self.microseconds.to_int64()
}
///|
fn PyTimeDelta::is_zero(self : PyTimeDelta) -> Bool {
self.days == 0L && self.seconds == 0 && self.microseconds == 0
}
///|
fn PyTimeDelta::compare(self : PyTimeDelta, other : PyTimeDelta) -> Int {
self.total_us().compare(other.total_us())
}
///|
fn check_td(td : PyTimeDelta) -> PyTimeDelta raise PyException {
if td.days < -999999999L || td.days > 999999999L {
raise PyException(
"OverflowError",
"days=\{td.days}; must have magnitude <= 999999999",
)
}
td
}
///|
fn pad(n : Int, width : Int) -> String {
let s = n.abs().to_string()
let s = if s.length() < width {
"0".repeat(width - s.length()) + s
} else {
s
}
if n < 0 {
"-" + s
} else {
s
}
}
///|
pub fn PyDate::isoformat(self : PyDate) -> String {
"\{pad(self.year, 4)}-\{pad(self.month, 2)}-\{pad(self.day, 2)}"
}
///|
pub fn PyTime::isoformat(self : PyTime) -> String {
let base = "\{pad(self.hour, 2)}:\{pad(self.minute, 2)}:\{pad(self.second, 2)}"
if self.microsecond != 0 {
base + "." + pad(self.microsecond, 6)
} else {
base
}
}
///|
fn tz_suffix(tz : Int?) -> String {
match tz {
None => ""
Some(off) => {
let sign = if off < 0 { "-" } else { "+" }
let o = off.abs()
let base = sign + pad(o / 3600, 2) + ":" + pad(o / 60 % 60, 2)
if o % 60 != 0 {
base + ":" + pad(o % 60, 2)
} else {
base
}
}
}
}
///|
pub fn PyDateTime::isoformat(self : PyDateTime, sep? : String = "T") -> String {
self.date.isoformat() + sep + self.time.isoformat() + tz_suffix(self.tz)
}
///|
pub fn PyDate::repr(self : PyDate) -> String {
"datetime.date(\{self.year}, \{self.month}, \{self.day})"
}
///|
fn time_repr_args(t : PyTime) -> String {
let mut s = "\{t.hour}, \{t.minute}"
if t.second != 0 || t.microsecond != 0 {
s = s + ", \{t.second}"
}
if t.microsecond != 0 {
s = s + ", \{t.microsecond}"
}
s
}
///|
pub fn PyTime::repr(self : PyTime) -> String {
"datetime.time(\{time_repr_args(self)})"
}
///|
pub fn PyDateTime::repr(self : PyDateTime) -> String {
let d = self.date
let tz = match self.tz {
None => ""
Some(0) => ", tzinfo=datetime.timezone.utc"
Some(off) =>
", tzinfo=datetime.timezone(datetime.timedelta(seconds=\{off}))"
}
"datetime.datetime(\{d.year}, \{d.month}, \{d.day}, \{time_repr_args(self.time)}\{tz})"
}
///|
pub fn PyTimeDelta::repr(self : PyTimeDelta) -> String {
let parts = []
if self.days != 0L {
parts.push("days=\{self.days}")
}
if self.seconds != 0 {
parts.push("seconds=\{self.seconds}")
}
if self.microseconds != 0 {
parts.push("microseconds=\{self.microseconds}")
}
if parts.is_empty() {
parts.push("0")
}
"datetime.timedelta(" + parts.join(", ") + ")"
}
///|
/// Python `str(timedelta)`.
pub fn PyTimeDelta::py_str(self : PyTimeDelta) -> String {
let mm = self.seconds / 60
let ss = self.seconds % 60
let hh = mm / 60
let mm = mm % 60
let mut s = "\{hh}:\{pad(mm, 2)}:\{pad(ss, 2)}"
if self.days != 0L {
let plural = if self.days.abs() != 1L { "s" } else { "" }
s = "\{self.days} day\{plural}, " + s
}
if self.microseconds != 0 {
s = s + "." + pad(self.microseconds, 6)
}
s
}
///|
/// `date + timedelta` (only whole days are used, as in Python).
fn PyDate::add_days(self : PyDate, days : Int64) -> PyDate raise PyException {
from_ordinal(self.toordinal() + days) catch {
_ => raise PyException("OverflowError", "date value out of range")
}
}
///|
fn PyDateTime::add_us(
self : PyDateTime,
us : Int64,
) -> PyDateTime raise PyException {
let total = self.total_us() + us
if total < 0L || total >= 3652059L * 86400000000L {
raise PyException("OverflowError", "date value out of range")
}
datetime_from_us(total, self.tz)
}
///|
/// Python `datetime.datetime.fromtimestamp(ts, datetime.timezone.utc)`.
fn datetime_from_timestamp_utc(ts : Value) -> PyDateTime raise PyException {
let us = match ts {
Int(i) => i * 1000000L
Bool(b) => if b { 1000000L } else { 0L }
Float(d) => {
if d.is_nan() || d.is_inf() {
raise value_error("Invalid value \{float_repr(d)}")
}
let secs = d.floor()
let frac = d - secs
// round half to even on the microseconds
let us = round_half_even(frac * 1000000.0)
secs.to_int64() * 1000000L + us.to_int64()
}
_ =>
raise type_error(
"'\{ts.type_name()}' object cannot be interpreted as an integer",
)
}
datetime_from_us(us + 62135596800L * 1000000L, Some(0))
}
///|
fn round_half_even(x : Double) -> Double {
let f = x.floor()
let diff = x - f
if diff > 0.5 {
f + 1.0
} else if diff < 0.5 {
f
} else if f % 2.0 == 0.0 {
f
} else {
f + 1.0
}
}
// ---- ISO format parsing (Python 3.11+ fromisoformat) ----
///|
fn digits_at(chars : Array[Char], start : Int, count : Int) -> Int? {
if start + count > chars.length() {
return None
}
let mut v = 0
for i in start..<(start + count) {
let c = chars[i]
if c < '0' || c > '9' {
return None
}
v = v * 10 + (c.to_int() - '0'.to_int())
}
Some(v)
}
///|
fn invalid_iso(s : String) -> PyException {
value_error("Invalid isoformat string: \{@core.py_repr_str(s)}")
}
///|
/// Parses the date part of an ISO string; returns the date and the number of chars used.
fn parse_iso_date(
chars : Array[Char],
s : String,
) -> (PyDate, Int) raise PyException {
guard digits_at(chars, 0, 4) is Some(year) else { raise invalid_iso(s) }
if chars.length() >= 5 && chars[4] == '-' {
if chars.length() >= 6 && chars[5] == 'W' {
// YYYY-Www[-D]
guard digits_at(chars, 6, 2) is Some(week) else { raise invalid_iso(s) }
let (dow, used) = if chars.length() >= 10 && chars[8] == '-' {
guard digits_at(chars, 9, 1) is Some(d) else { raise invalid_iso(s) }
(d, 10)
} else {
(1, 8)
}
return (iso_calendar_date(year, week, dow, s), used)
}
guard digits_at(chars, 5, 2) is Some(month) &&
chars.length() >= 10 &&
chars[7] == '-' &&
digits_at(chars, 8, 2) is Some(day) else {
raise invalid_iso(s)
}
(
check_date(year, month, day) catch {
PyException(_, m) => raise value_error(m)
},
10,
)
} else {
guard digits_at(chars, 4, 2) is Some(month) &&
digits_at(chars, 6, 2) is Some(day) else {
raise invalid_iso(s)
}
(check_date(year, month, day), 8)
}
}
///|
fn iso_calendar_date(
year : Int,
week : Int,
dow : Int,
s : String,
) -> PyDate raise PyException {
if week < 1 || week > 53 || dow < 1 || dow > 7 {
raise invalid_iso(s)
}
// ordinal of the Monday of ISO week 1
let jan4 = ordinal(year, 1, 4)
let week1_monday = jan4 - (jan4 + 6L) % 7L
from_ordinal(week1_monday + ((week - 1) * 7 + dow - 1).to_int64())
}
///|
/// Parses `HH[:MM[:SS[.ffffff]]]` (or the basic format) followed by an optional UTC
/// offset; returns the time and the offset in seconds.
fn parse_iso_time(
chars : Array[Char],
start : Int,
s : String,
) -> (PyTime, Int?) raise PyException {
// split off the time zone
let mut end = chars.length()
let mut tz : Int? = None
for i in start.. (PyTime, Int) raise PyException {
guard digits_at(chars, start, 2) is Some(hour) else { raise invalid_iso(s) }
let mut pos = start + 2
let mut minute = 0
let mut second = 0
let mut micro = 0
let extended = pos < chars.length() && chars[pos] == ':'
fn component(p : Int) -> (Int, Int)? {
let p = if extended {
if p < chars.length() && chars[p] == ':' {
p + 1
} else {
return None
}
} else {
p
}
match digits_at(chars, p, 2) {
Some(v) => Some((v, p + 2))
None => None
}
}
match component(pos) {
Some((v, p)) => {
minute = v
pos = p
match component(pos) {
Some((v, p)) => {
second = v
pos = p
if pos < chars.length() && (chars[pos] == '.' || chars[pos] == ',') {
pos += 1
let mut n = 0
let mut frac = 0
while pos < chars.length() && chars[pos] >= '0' && chars[pos] <= '9' {
if n < 6 {
frac = frac * 10 + (chars[pos].to_int() - '0'.to_int())
}
n += 1
pos += 1
}
if n == 0 {
raise invalid_iso(s)
}
let mut k = n
while k < 6 {
frac = frac * 10
k += 1
}
micro = frac
}
}
None => ()
}
}
None => ()
}
(
check_time(hour, minute, second, micro) catch {
PyException(_, m) => raise value_error(m)
},
pos,
)
}
///|
/// Python `datetime.date.fromisoformat(s)`.
pub fn date_fromisoformat(v : Value) -> PyDate raise PyException {
guard v is Str(s) else {
raise type_error("fromisoformat: argument must be str")
}
let chars = s.to_array()
let (d, used) = parse_iso_date(chars, s)
if used != chars.length() {
raise invalid_iso(s)
}
d
}
///|
/// Python `datetime.datetime.fromisoformat(s)`.
pub fn datetime_fromisoformat(v : Value) -> PyDateTime raise PyException {
guard v is Str(s) else {
raise type_error("fromisoformat: argument must be str")
}
let chars = s.to_array()
let (d, used) = parse_iso_date(chars, s)
if used == chars.length() {
return {
date: d,
time: { hour: 0, minute: 0, second: 0, microsecond: 0, },
tz: None,
}
}
// any single separator character
let (t, tz) = parse_iso_time(chars, used + 1, s)
{ date: d, time: t, tz, }
}
///|
/// Python `datetime.time.fromisoformat(s)`.
pub fn time_fromisoformat(v : Value) -> PyTime raise PyException {
guard v is Str(s) else {
raise type_error("fromisoformat: argument must be str")
}
let chars = s.to_array()
let start = if chars.length() > 0 && chars[0] == 'T' { 1 } else { 0 }
let (t, _) = parse_iso_time(chars, start, s)
t
}
// ---- strftime / strptime ----
///|
let month_names : Array[String] = [
"January", "February", "March", "April", "May", "June", "July", "August", "September",
"October", "November", "December",
]
///|
let day_names : Array[String] = [
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday",
]
///|
/// Python `datetime.strftime(fmt)` (C locale).
pub fn strftime(dt : PyDateTime, fmt : String) -> String {
let d = dt.date
let t = dt.time
let sb = StringBuilder()
let chars = fmt.to_array()
let mut i = 0
while i < chars.length() {
let c = chars[i]
if c != '%' || i + 1 >= chars.length() {
sb.write_char(c)
i += 1
continue
}
let f = chars[i + 1]
i += 2
let yday = (d.toordinal() - ordinal(d.year, 1, 1) + 1L).to_int()
let wday = d.weekday()
match f {
'Y' => sb.write_string(d.year.to_string())
'y' => sb.write_string(pad(d.year % 100, 2))
'm' => sb.write_string(pad(d.month, 2))
'd' => sb.write_string(pad(d.day, 2))
'e' => {
let s = d.day.to_string()
sb.write_string(if s.length() < 2 { " " + s } else { s })
}
'H' => sb.write_string(pad(t.hour, 2))
'I' => {
let h = t.hour % 12
sb.write_string(pad(if h == 0 { 12 } else { h }, 2))
}
'p' => sb.write_string(if t.hour < 12 { "AM" } else { "PM" })
'M' => sb.write_string(pad(t.minute, 2))
'S' => sb.write_string(pad(t.second, 2))
'f' => sb.write_string(pad(t.microsecond, 6))
'j' => sb.write_string(pad(yday, 3))
'b' | 'h' => sb.write_string(month_names[d.month - 1].substring(end=3))
'B' => sb.write_string(month_names[d.month - 1])
'a' => sb.write_string(day_names[wday].substring(end=3))
'A' => sb.write_string(day_names[wday])
'w' => sb.write_string(((wday + 1) % 7).to_string())
'u' => sb.write_string((wday + 1).to_string())
'U' => sb.write_string(pad((yday + 6 - (wday + 1) % 7) / 7, 2))
'W' => sb.write_string(pad((yday + 6 - wday) / 7, 2))
'F' => sb.write_string(d.isoformat())
'T' =>
sb.write_string(
"\{pad(t.hour, 2)}:\{pad(t.minute, 2)}:\{pad(t.second, 2)}",
)
'D' =>
sb.write_string(
"\{pad(d.month, 2)}/\{pad(d.day, 2)}/\{pad(d.year % 100, 2)}",
)
'z' =>
match dt.tz {
None => ()
Some(off) => {
let sign = if off < 0 { "-" } else { "+" }
let o = off.abs()
sb.write_string(sign + pad(o / 3600, 2) + pad(o / 60 % 60, 2))
}
}
'Z' => if dt.tz is Some(_) { sb.write_string("UTC") }
'%' => sb.write_char('%')
_ => {
sb.write_char('%')
sb.write_char(f)
}
}
}
sb.to_string()
}
///|
/// Python `datetime.datetime.strptime(s, fmt)` for the common directives.
pub fn strptime(s : String, fmt : String) -> PyDateTime raise PyException {
let text = s.to_array()
let f = fmt.to_array()
let mut pos = 0
let mut year = 1900
let mut month = 1
let mut day = 1
let mut hour = 0
let mut minute = 0
let mut second = 0
let mut micro = 0
let mut pm : Bool? = None
let mut yday : Int? = None
let mut tz : Int? = None
let fail = fn() {
value_error(
"time data \{@core.py_repr_str(s)} does not match format \{@core.py_repr_str(fmt)}",
)
}
fn num(max_len : Int) -> Int raise PyException {
let start = pos
let mut v = 0
while pos < text.length() &&
pos - start < max_len &&
text[pos] >= '0' &&
text[pos] <= '9' {
v = v * 10 + (text[pos].to_int() - '0'.to_int())
pos += 1
}
if pos == start {
raise fail()
}
v
}
fn name(names : Array[String], abbrev : Bool) -> Int raise PyException {
for i, n in names {
let cand = if abbrev { n.substring(end=3) } else { n }
let cl = cand.length()
if pos + cl <= text.length() &&
@core.py_lower(String::from_array(text[pos:pos + cl].to_array())) ==
@core.py_lower(cand) {
pos += cl
return i
}
}
raise fail()
}
let mut i = 0
while i < f.length() {
let c = f[i]
if c == '%' && i + 1 < f.length() {
let d = f[i + 1]
i += 2
match d {
'Y' => year = num(4)
'y' => {
let v = num(2)
year = if v < 69 { 2000 + v } else { 1900 + v }
}
'm' => month = num(2)
'd' => day = num(2)
'H' => hour = num(2)
'I' => hour = num(2)
'M' => minute = num(2)
'S' => second = num(2)
'f' => {
let start = pos
let v = num(6)
let mut k = pos - start
let mut frac = v
while k < 6 {
frac = frac * 10
k += 1
}
micro = frac
}
'j' => yday = Some(num(3))
'p' => {
let rest = @core.py_lower(String::from_array(text[pos:].to_array()))
if rest.has_prefix("am") {
pm = Some(false)
} else if rest.has_prefix("pm") {
pm = Some(true)
} else {
raise fail()
}
pos += 2
}
'b' | 'h' => month = name(month_names, true) + 1
'B' => month = name(month_names, false) + 1
'a' => ignore(name(day_names, true))
'A' => ignore(name(day_names, false))
'z' =>
if pos < text.length() && text[pos] == 'Z' {
pos += 1
tz = Some(0)
} else {
guard pos < text.length() && (text[pos] == '+' || text[pos] == '-') else {
raise fail()
}
let sign = if text[pos] == '-' { -1 } else { 1 }
pos += 1
let hh = num(2)
if pos < text.length() && text[pos] == ':' {
pos += 1
}
let mm = num(2)
tz = Some(sign * (hh * 3600 + mm * 60))
}
'%' => {
guard pos < text.length() && text[pos] == '%' else { raise fail() }
pos += 1
}
_ => raise value_error("'\{d}' is a bad directive in format '\{fmt}'")
}
} else if c == ' ' || c == '\t' || c == '\n' {
// whitespace in the format matches any run of whitespace
while pos < text.length() &&
(text[pos] == ' ' || text[pos] == '\t' || text[pos] == '\n') {
pos += 1
}
i += 1
} else {
guard pos < text.length() && text[pos] == c else { raise fail() }
pos += 1
i += 1
}
}
if pos != text.length() {
raise value_error(
"unconverted data remains: \{String::from_array(text[pos:].to_array())}",
)
}
match pm {
Some(true) => if hour != 12 { hour += 12 }
Some(false) => if hour == 12 { hour = 0 }
None => ()
}
let date = match yday {
Some(n) => from_ordinal(ordinal(year, 1, 1) + (n - 1).to_int64())
None => check_date(year, month, day)
}
{ date, time: check_time(hour, minute, second, micro), tz, }
}