Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Morrow.fromordinal #5

Merged
merged 1 commit into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions morrow/constants.mojo
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from ._py import py_dt_datetime

# todo: hardcode for tmp
alias _MAX_TIMESTAMP: Int = 32503737600
alias MAX_TIMESTAMP = _MAX_TIMESTAMP
alias MAX_TIMESTAMP_MS = MAX_TIMESTAMP * 1000
alias MAX_TIMESTAMP_US = MAX_TIMESTAMP * 1_000_000

alias _DAYS_IN_MONTH = VariadicList[Int](
-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
)
alias _DAYS_BEFORE_MONTH = VariadicList[Int](
-1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334
) # -1 is a placeholder for indexing purposes.
113 changes: 97 additions & 16 deletions morrow/morrow.mojo
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
from ._py import py_dt_datetime
from .util import normalize_timestamp, num2str, _ymd2ord
from .util import normalize_timestamp, num2str, _ymd2ord, _days_before_year
from ._libc import c_gettimeofday, c_localtime, c_gmtime, c_strptime
from ._libc import CTimeval, CTm
from .timezone import TimeZone
from .timedelta import TimeDelta
from .constants import _DAYS_BEFORE_MONTH, _DAYS_IN_MONTH


alias _DI400Y = 146097 # number of days in 400 years
alias _DI100Y = 36524 # " " " " 100 "
alias _DI4Y = 1461 # " " " " 4 "


@value
Expand Down Expand Up @@ -38,17 +44,17 @@ struct Morrow:
self.TimeZone = TimeZone

@staticmethod
fn now() raises -> Morrow:
fn now() raises -> Self:
let t = c_gettimeofday()
return Morrow._fromtimestamp(t, False)
return Self._fromtimestamp(t, False)

@staticmethod
fn utcnow() raises -> Morrow:
fn utcnow() raises -> Self:
let t = c_gettimeofday()
return Morrow._fromtimestamp(t, True)
return Self._fromtimestamp(t, True)

@staticmethod
fn _fromtimestamp(t: CTimeval, utc: Bool) raises -> Morrow:
fn _fromtimestamp(t: CTimeval, utc: Bool) raises -> Self:
let tm: CTm
let tz: TimeZone
if utc:
Expand All @@ -58,7 +64,7 @@ struct Morrow:
tm = c_localtime(t.tv_sec)
tz = TimeZone(tm.tm_gmtoff.to_int(), "local")

let result = Morrow(
let result = Self(
tm.tm_year.to_int() + 1900,
tm.tm_mon.to_int() + 1,
tm.tm_mday.to_int(),
Expand All @@ -71,21 +77,21 @@ struct Morrow:
return result

@staticmethod
fn fromtimestamp(timestamp: Float64) raises -> Morrow:
fn fromtimestamp(timestamp: Float64) raises -> Self:
let timestamp_ = normalize_timestamp(timestamp)
let t = CTimeval(timestamp_.to_int())
return Morrow._fromtimestamp(t, False)
return Self._fromtimestamp(t, False)

@staticmethod
fn utcfromtimestamp(timestamp: Float64) raises -> Morrow:
fn utcfromtimestamp(timestamp: Float64) raises -> Self:
let timestamp_ = normalize_timestamp(timestamp)
let t = CTimeval(timestamp_.to_int())
return Morrow._fromtimestamp(t, True)
return Self._fromtimestamp(t, True)

@staticmethod
fn strptime(
date_str: String, fmt: String, tzinfo: TimeZone = TimeZone.none()
) raises -> Morrow:
) raises -> Self:
"""
Create a Morrow instance from a date string and format,
in the style of ``datetime.strptime``. Optionally replaces the parsed TimeZone.
Expand All @@ -97,7 +103,7 @@ struct Morrow:
"""
let tm = c_strptime(date_str, fmt)
let tz = TimeZone(tm.tm_gmtoff.to_int()) if tzinfo.is_none() else tzinfo
return Morrow(
return Self(
tm.tm_year.to_int() + 1900,
tm.tm_mon.to_int() + 1,
tm.tm_mday.to_int(),
Expand All @@ -109,7 +115,7 @@ struct Morrow:
)

@staticmethod
fn strptime(date_str: String, fmt: String, tz_str: String) raises -> Morrow:
fn strptime(date_str: String, fmt: String, tz_str: String) raises -> Self:
"""
Create a Morrow instance by time_zone_string with utc format

Expand All @@ -119,7 +125,7 @@ struct Morrow:
<Morrow [2019-01-20T15:49:10+08:00]>
"""
let tzinfo = TimeZone.from_utc(tz_str)
return Morrow.strptime(date_str, fmt, tzinfo)
return Self.strptime(date_str, fmt, tzinfo)

fn isoformat(
self, sep: String = "T", timespec: StringLiteral = "auto"
Expand Down Expand Up @@ -193,10 +199,85 @@ struct Morrow:
"""
return _ymd2ord(self.year, self.month, self.day)

@staticmethod
fn fromordinal(ordinal: Int) raises -> Self:
"""Construct a Morrow from a proleptic Gregorian ordinal.

January 1 of year 1 is day 1. Only the year, month and day are
non-zero in the result.
"""
# n is a 1-based index, starting at 1-Jan-1. The pattern of leap years
# repeats exactly every 400 years. The basic strategy is to find the
# closest 400-year boundary at or before n, then work with the offset
# from that boundary to n. Life is much clearer if we subtract 1 from
# n first -- then the values of n at 400-year boundaries are exactly
# those divisible by _DI400Y:
#
# D M Y n n-1
# -- --- ---- ---------- ----------------
# 31 Dec -400 -_DI400Y -_DI400Y -1
# 1 Jan -399 -_DI400Y +1 -_DI400Y 400-year boundary
# ...
# 30 Dec 000 -1 -2
# 31 Dec 000 0 -1
# 1 Jan 001 1 0 400-year boundary
# 2 Jan 001 2 1
# 3 Jan 001 3 2
# ...
# 31 Dec 400 _DI400Y _DI400Y -1
# 1 Jan 401 _DI400Y +1 _DI400Y 400-year boundary
var n = ordinal
n -= 1
let n400 = n // _DI400Y
n = n % _DI400Y
var year = n400 * 400 + 1 # ..., -399, 1, 401, ...

# Now n is the (non-negative) offset, in days, from January 1 of year, to
# the desired date. Now compute how many 100-year cycles precede n.
# Note that it's possible for n100 to equal 4! In that case 4 full
# 100-year cycles precede the desired day, which implies the desired
# day is December 31 at the end of a 400-year cycle.
let n100 = n // _DI100Y
n = n % _DI100Y

# Now compute how many 4-year cycles precede it.
let n4 = n // _DI4Y
n = n % _DI4Y

# And now how many single years. Again n1 can be 4, and again meaning
# that the desired day is December 31 at the end of the 4-year cycle.
let n1 = n // 365
n = n % 365

year += n100 * 100 + n4 * 4 + n1
if n1 == 4 or n100 == 4:
return Self(year - 1, 12, 31)

# Now the year is correct, and n is the offset from January 1. We find
# the month via an estimate that's either exact or one too large.
let leapyear = n1 == 3 and (n4 != 24 or n100 == 3)
var month = (n + 50) >> 5
var preceding: Int
if month > 2 and leapyear:
preceding = _DAYS_BEFORE_MONTH[month] + 1
else:
preceding = _DAYS_BEFORE_MONTH[month]
if preceding > n: # estimate is too large
month -= 1
if month == 2 and leapyear:
preceding -= (_DAYS_BEFORE_MONTH[month] + 1)
else:
preceding -= _DAYS_BEFORE_MONTH[month]
n -= preceding

# Now the year and month are correct, and n is the offset from the
# start of that month: we're done!
return Self(year, month, n+1)

fn __str__(self) raises -> String:
return self.isoformat()

fn __sub__(self, other: Morrow) raises -> TimeDelta:
fn __sub__(self, other: Self) raises -> TimeDelta:
let days1 = self.toordinal()
let days2 = other.toordinal()
let secs1 = self.second + self.minute * 60 + self.hour * 3600
Expand Down
9 changes: 1 addition & 8 deletions morrow/util.mojo
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
from .constants import MAX_TIMESTAMP, MAX_TIMESTAMP_MS, MAX_TIMESTAMP_US


alias _DAYS_IN_MONTH = VariadicList[Int](
-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
)
alias _DAYS_BEFORE_MONTH = VariadicList[Int](
-1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334
) # -1 is a placeholder for indexing purposes.
from .constants import _DAYS_IN_MONTH, _DAYS_BEFORE_MONTH


fn _is_leap(year: Int) -> Bool:
Expand Down
11 changes: 11 additions & 0 deletions test.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ def test_strptime():
m = Morrow.strptime("2023-10-18 15:49:10", "%Y-%m-%d %H:%M:%S", "+09:00")
assert_equal(m.__str__(), "2023-10-18T15:49:10.000000+09:00")

def test_ordinal():
print("Running test_ordinal()")
m = Morrow(2023, 10, 1)
o = m.toordinal()
assert_equal(o, 738794)

m2 = Morrow.fromordinal(o)
assert_equal(m2.year, 2023)
assert_equal(m.month, 10)
assert_equal(m.day, 1)


def test_sub():
print("Running test_sub()")
Expand Down