Browse Source

Merge branch 'master' of https://github.com/odin-lang/Odin

gingerBill 1 year ago
parent
commit
fc587c507a

+ 77 - 0
core/time/datetime/constants.odin

@@ -0,0 +1,77 @@
+package datetime
+
+// Ordinal 1 = Midnight Monday, January 1, 1 A.D. (Gregorian)
+//         |   Midnight Monday, January 3, 1 A.D. (Julian)
+Ordinal :: i64
+EPOCH   :: Ordinal(1)
+
+// Minimum and maximum dates and ordinals. Chosen for safe roundtripping.
+MIN_DATE :: Date{year = -25_252_734_927_766_552, month =  1, day =  1}
+MAX_DATE :: Date{year =  25_252_734_927_766_552, month = 12, day = 31}
+MIN_ORD  :: Ordinal(-9_223_372_036_854_775_234)
+MAX_ORD  :: Ordinal( 9_223_372_036_854_774_869)
+
+Error :: enum {
+	None,
+	Invalid_Year,
+	Invalid_Month,
+	Invalid_Day,
+	Invalid_Hour,
+	Invalid_Minute,
+	Invalid_Second,
+	Invalid_Nano,
+	Invalid_Ordinal,
+	Invalid_Delta,
+}
+
+Date :: struct {
+	year:   i64,
+	month:  i8,
+	day:    i8,
+}
+
+Time :: struct {
+	hour:   i8,
+	minute: i8,
+	second: i8,
+	nano:   i32,
+}
+
+DateTime :: struct {
+	using date: Date,
+	using time: Time,
+}
+
+Delta :: struct {
+	days:    i64, // These are all i64 because we can also use it to add a number of seconds or nanos to a moment,
+	seconds: i64, // that are then normalized within their respective ranges.
+	nanos:   i64,
+}
+
+Month :: enum i8 {
+	January = 1,
+	February,
+	March,
+	April,
+	May,
+	June,
+	July,
+	August,
+	September,
+	October,
+	November,
+	December,
+}
+
+Weekday :: enum i8 {
+	Sunday = 0,
+	Monday,
+	Tuesday,
+	Wednesday,
+	Thursday,
+	Friday,
+	Saturday,
+}
+
+@(private)
+MONTH_DAYS :: [?]i8{-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}

+ 272 - 0
core/time/datetime/datetime.odin

@@ -0,0 +1,272 @@
+/*
+	Calendrical conversions using a proleptic Gregorian calendar.
+
+	Implemented using formulas from: Calendrical Calculations Ultimate Edition, Reingold & Dershowitz
+*/
+package datetime
+
+import "base:intrinsics"
+
+// Procedures that return an Ordinal
+
+date_to_ordinal :: proc "contextless" (date: Date) -> (ordinal: Ordinal, err: Error) {
+	validate(date) or_return
+	return unsafe_date_to_ordinal(date), .None
+}
+
+components_to_ordinal :: proc "contextless" (#any_int year, #any_int month, #any_int day: i64) -> (ordinal: Ordinal, err: Error) {
+	validate(year, month, day) or_return
+	return unsafe_date_to_ordinal({year, i8(month), i8(day)}), .None
+}
+
+// Procedures that return a Date
+
+ordinal_to_date :: proc "contextless" (ordinal: Ordinal) -> (date: Date, err: Error) {
+	validate(ordinal) or_return
+	return unsafe_ordinal_to_date(ordinal), .None
+}
+
+components_to_date :: proc "contextless" (#any_int year, #any_int month, #any_int day: i64) -> (date: Date, err: Error) {
+	validate(year, month, day) or_return
+	return Date{i64(year), i8(month), i8(day)}, .None
+}
+
+components_to_time :: proc "contextless" (#any_int hour, #any_int minute, #any_int second: i64, #any_int nanos := i64(0)) -> (time: Time, err: Error) {
+	validate(hour, minute, second, nanos) or_return
+	return Time{i8(hour), i8(minute), i8(second), i32(nanos)}, .None
+}
+
+components_to_datetime :: proc "contextless" (#any_int year, #any_int month, #any_int day, #any_int hour, #any_int minute, #any_int second: i64, #any_int nanos := i64(0)) -> (datetime: DateTime, err: Error) {
+	date := components_to_date(year, month, day)            or_return
+	time := components_to_time(hour, minute, second, nanos) or_return
+	return {date, time}, .None
+}
+
+ordinal_to_datetime :: proc "contextless" (ordinal: Ordinal) -> (datetime: DateTime, err: Error) {
+	d := ordinal_to_date(ordinal) or_return
+	return {Date(d), {}}, .None
+}
+
+day_of_week :: proc "contextless" (ordinal: Ordinal) -> (day: Weekday) {
+	return Weekday((ordinal - EPOCH) %% 7)
+}
+
+subtract_dates :: proc "contextless" (a, b: Date) -> (delta: Delta, err: Error) {
+	ord_a := date_to_ordinal(a) or_return
+	ord_b := date_to_ordinal(b) or_return
+
+	delta  = Delta{days=ord_a - ord_b}
+	return
+}
+
+subtract_datetimes :: proc "contextless" (a, b: DateTime) -> (delta: Delta, err: Error) {
+	ord_a := date_to_ordinal(a) or_return
+	ord_b := date_to_ordinal(b) or_return
+
+	validate(a.time) or_return
+	validate(b.time) or_return
+
+	seconds_a := i64(a.hour) * 3600 + i64(a.minute) * 60 + i64(a.second)
+	seconds_b := i64(b.hour) * 3600 + i64(b.minute) * 60 + i64(b.second)
+
+	delta = Delta{ord_a - ord_b, seconds_a - seconds_b, i64(a.nano) - i64(b.nano)}
+	return
+}
+
+subtract_deltas :: proc "contextless" (a, b: Delta) -> (delta: Delta, err: Error) {
+	delta = Delta{a.days - b.days, a.seconds - b.seconds, a.nanos - b.nanos}
+	delta = normalize_delta(delta) or_return
+	return
+}
+sub :: proc{subtract_datetimes, subtract_dates, subtract_deltas}
+
+add_days_to_date :: proc "contextless" (a: Date, days: i64) -> (date: Date, err: Error) {
+	ord := date_to_ordinal(a) or_return
+	ord += days
+	return ordinal_to_date(ord)
+}
+
+add_delta_to_date :: proc "contextless" (a: Date, delta: Delta) -> (date: Date, err: Error) {
+	ord := date_to_ordinal(a) or_return
+	// Because the input is a Date, we add only the days from the Delta.
+	ord += delta.days
+	return ordinal_to_date(ord)
+}
+
+add_delta_to_datetime :: proc "contextless" (a: DateTime, delta: Delta) -> (datetime: DateTime, err: Error) {
+	days   := date_to_ordinal(a) or_return
+
+	a_seconds := i64(a.hour) * 3600 + i64(a.minute) * 60 + i64(a.second)
+	a_delta   := Delta{days=days, seconds=a_seconds, nanos=i64(a.nano)}
+
+	sum_delta := Delta{days=a_delta.days + delta.days, seconds=a_delta.seconds + delta.seconds, nanos=a_delta.nanos + delta.nanos}
+	sum_delta  = normalize_delta(sum_delta) or_return
+
+	datetime.date = ordinal_to_date(sum_delta.days) or_return
+
+	hour,   rem    := divmod(sum_delta.seconds, 3600)
+	minute, second := divmod(rem, 60)
+
+	datetime.time = components_to_time(hour, minute, second, sum_delta.nanos) or_return
+	return
+}
+add :: proc{add_days_to_date, add_delta_to_date, add_delta_to_datetime}
+
+day_number :: proc "contextless" (date: Date) -> (day_number: i64, err: Error) {
+	validate(date) or_return
+
+	ord := unsafe_date_to_ordinal(date)
+	_, day_number = unsafe_ordinal_to_year(ord)
+	return
+}
+
+days_remaining :: proc "contextless" (date: Date) -> (days_remaining: i64, err: Error) {
+	// Alternative formulation `day_number` subtracted from 365 or 366 depending on leap year
+	validate(date) or_return
+	delta := sub(date, Date{date.year, 12, 31}) or_return
+	return delta.days, .None
+}
+
+last_day_of_month :: proc "contextless" (#any_int year: i64, #any_int month: i8) -> (day: i64, err: Error) {
+	// Not using formula 2.27 from the book. This is far simpler and gives the same answer.
+
+	validate(Date{year, month, 1}) or_return
+	month_days := MONTH_DAYS
+
+	day = i64(month_days[month])
+	if month == 2 && is_leap_year(year) {
+		day += 1
+	}
+	return
+}
+
+new_year :: proc "contextless" (#any_int year: i64) -> (new_year: Date, err: Error) {
+	validate(year, 1, 1) or_return
+	return {year, 1, 1}, .None
+}
+
+year_end :: proc "contextless" (#any_int year: i64) -> (year_end: Date, err: Error) {
+	validate(year, 12, 31) or_return
+	return {year, 12, 31}, .None
+}
+
+year_range :: proc (#any_int year: i64, allocator := context.allocator) -> (range: []Date) {
+	is_leap := is_leap_year(year)
+
+	days := 366 if is_leap else 365
+	range = make([]Date, days, allocator)
+
+	month_days := MONTH_DAYS
+	if is_leap {
+		month_days[2] = 29
+	}
+
+	i := 0
+	for month in 1..=len(month_days) {
+		for day in 1..=month_days[month] {
+			range[i], _ = components_to_date(year, month, day)
+			i += 1
+		}
+	}
+	return
+}
+
+normalize_delta :: proc "contextless" (delta: Delta) -> (normalized: Delta, err: Error) {
+	// Distribute nanos into seconds and remainder
+	seconds, nanos := divmod(delta.nanos, 1e9)
+
+	// Add original seconds to rolled over seconds.
+	seconds += delta.seconds
+	days: i64
+
+	// Distribute seconds into number of days and remaining seconds.
+	days, seconds = divmod(seconds, 24 * 3600)
+
+	// Add original days
+	days += delta.days
+
+	if days <= MIN_ORD || days >= MAX_ORD {
+		return {}, .Invalid_Delta
+	}
+	return Delta{days, seconds, nanos}, .None
+}
+
+// The following procedures don't check whether their inputs are in a valid range.
+// They're still exported for those who know their inputs have been validated.
+
+unsafe_date_to_ordinal :: proc "contextless" (date: Date) -> (ordinal: Ordinal) {
+	year_minus_one := date.year - 1
+
+	// Day before epoch
+	ordinal = EPOCH - 1
+
+	// Add non-leap days
+	ordinal += 365 * year_minus_one
+
+	// Add leap days
+	ordinal += floor_div(year_minus_one, 4)          // Julian-rule leap days
+	ordinal -= floor_div(year_minus_one, 100)        // Prior century years
+	ordinal += floor_div(year_minus_one, 400)        // Prior 400-multiple years
+	ordinal += floor_div(367 * i64(date.month) - 362, 12) // Prior days this year
+
+	// Apply correction
+	if date.month <= 2 {
+		ordinal += 0
+	} else if is_leap_year(date.year) {
+		ordinal -= 1
+	} else {
+		ordinal -= 2
+	}
+
+	// Add days
+	ordinal += i64(date.day)
+	return
+}
+
+unsafe_ordinal_to_year :: proc "contextless" (ordinal: Ordinal) -> (year: i64, day_ordinal: i64) {
+	// Days after epoch
+	d0   := ordinal - EPOCH
+
+	// Number of 400-year cycles and remainder
+	n400, d1 := divmod(d0, 146097)
+
+	// Number of 100-year cycles and remainder
+	n100, d2 := divmod(d1, 36524)
+
+	// Number of 4-year cycles and remainder
+	n4,   d3 := divmod(d2, 1461)
+
+	// Number of remaining days
+	n1,   d4 := divmod(d3, 365)
+
+	year  = 400 * n400 + 100 * n100 + 4 * n4 + n1
+
+	if n1 != 4 && n100 != 4 {
+		day_ordinal = d4 + 1
+	} else {
+		day_ordinal = 366
+	}
+
+	if n100 == 4 || n1 == 4 {
+		return year, day_ordinal
+	}
+	return year + 1, day_ordinal
+}
+
+unsafe_ordinal_to_date :: proc "contextless" (ordinal: Ordinal) -> (date: Date) {
+	year, _ := unsafe_ordinal_to_year(ordinal)
+
+	prior_days := ordinal - unsafe_date_to_ordinal(Date{year, 1, 1})
+	correction := Ordinal(2)
+
+	if ordinal < unsafe_date_to_ordinal(Date{year, 3, 1}) {
+		correction = 0
+	} else if is_leap_year(year) {
+		correction = 1
+	}
+
+	month := i8(floor_div((12 * (prior_days + correction) + 373), 367))
+	day   := i8(ordinal - unsafe_date_to_ordinal(Date{year, month, 1}) + 1)
+
+	return {year, month, day}
+}

+ 95 - 0
core/time/datetime/internal.odin

@@ -0,0 +1,95 @@
+package datetime
+
+// Internal helper functions for calendrical conversions
+
+import "base:intrinsics"
+
+sign :: proc "contextless" (v: i64) -> (res: i64) {
+	if v == 0 {
+		return 0
+	} else if v > 0 {
+		return 1
+	}
+	return -1
+}
+
+// Caller has to ensure y != 0
+divmod :: proc "contextless" (x, y: $T, loc := #caller_location) -> (a: T, r: T)
+	where intrinsics.type_is_integer(T) {
+	a = x / y
+	r = x % y
+	if (r > 0 && y < 0) || (r < 0 && y > 0) {
+		a -= 1
+		r += y
+	}
+	return a, r
+}
+
+// Divides and floors
+floor_div :: proc "contextless" (x, y: $T) -> (res: T)
+	where intrinsics.type_is_integer(T) {
+	res = x / y
+	r  := x % y
+	if (r > 0 && y < 0) || (r < 0 && y > 0) {
+		res -= 1
+	}
+	return res
+}
+
+// Half open: x mod [1..b]
+interval_mod :: proc "contextless" (x, a, b: i64) -> (res: i64) {
+	if a == b {
+		return x
+	}
+	return a + ((x - a) %% (b - a))
+}
+
+// x mod [1..b]
+adjusted_remainder :: proc "contextless" (x, b: i64) -> (res: i64) {
+	m := x %% b
+	return b if m == 0 else m
+}
+
+gcd :: proc "contextless" (x, y: i64) -> (res: i64) {
+	if y == 0 {
+		return x
+	}
+
+	m := x %% y
+	return gcd(y, m)
+}
+
+lcm :: proc "contextless" (x, y: i64) -> (res: i64) {
+	return x * y / gcd(x, y)
+}
+
+sum :: proc "contextless" (i: i64, f: proc "contextless" (n: i64) -> i64, cond: proc "contextless" (n: i64) -> bool) -> (res: i64) {
+	for idx := i; cond(idx); idx += 1 {
+		res += f(idx)
+	}
+	return
+}
+
+product :: proc "contextless" (i: i64, f: proc "contextless" (n: i64) -> i64, cond: proc "contextless" (n: i64) -> bool) -> (res: i64) {
+	res = 1
+	for idx := i; cond(idx); idx += 1 {
+		res *= f(idx)
+	}
+	return
+}
+
+smallest :: proc "contextless" (k: i64, cond: proc "contextless" (n: i64) -> bool) -> (d: i64) {
+	k := k
+	for !cond(k) {
+		k += 1
+	}
+	return k
+}
+
+biggest :: proc "contextless" (k: i64, cond: proc "contextless" (n: i64) -> bool) -> (d: i64) {
+	k := k
+	for !cond(k) {
+		k -= 1
+	}
+	return k
+}

+ 72 - 0
core/time/datetime/validation.odin

@@ -0,0 +1,72 @@
+package datetime
+
+// Validation helpers
+is_leap_year :: proc "contextless" (#any_int year: i64) -> (leap: bool) {
+	return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
+}
+
+validate_date :: proc "contextless" (date: Date) -> (err: Error) {
+	return validate(date.year, date.month, date.day)
+}
+
+validate_year_month_day :: proc "contextless" (#any_int year, #any_int month, #any_int day: i64) -> (err: Error) {
+	if year < MIN_DATE.year || year > MAX_DATE.year {
+		return .Invalid_Year
+	}
+	if month < 1 || month > 12 {
+		return .Invalid_Month
+	}
+
+	month_days := MONTH_DAYS
+	days_this_month := month_days[month]
+	if month == 2 && is_leap_year(year) {
+		days_this_month = 29
+	}
+
+	if day < 1 || day > i64(days_this_month) {
+		return .Invalid_Day
+	}
+	return .None
+}
+
+validate_ordinal :: proc "contextless" (ordinal: Ordinal) -> (err: Error) {
+	if ordinal < MIN_ORD || ordinal > MAX_ORD {
+		return .Invalid_Ordinal
+	}
+	return
+}
+
+validate_time :: proc "contextless" (time: Time) -> (err: Error) {
+	return validate(time.hour, time.minute, time.second, time.nano)
+}
+
+validate_hour_minute_second :: proc "contextless" (#any_int hour, #any_int minute, #any_int second, #any_int nano: i64) -> (err: Error) {
+	if hour < 0 || hour > 23 {
+		return .Invalid_Hour
+	}
+	if minute < 0 || minute > 59 {
+		return .Invalid_Minute
+	}
+	if second < 0 || second > 59 {
+		return .Invalid_Second
+	}
+	if nano < 0 || nano > 1e9 {
+		return .Invalid_Nano
+	}
+	return .None
+}
+
+validate_datetime :: proc "contextless" (using datetime: DateTime) -> (err: Error) {
+	validate(date) or_return
+	validate(time) or_return
+	return .None
+}
+
+validate :: proc{
+	validate_date,
+	validate_year_month_day,
+	validate_ordinal,
+	validate_hour_minute_second,
+	validate_time,
+	validate_datetime,
+}

+ 122 - 0
core/time/rfc3339.odin

@@ -0,0 +1,122 @@
+package time
+// Parsing RFC 3339 date/time strings into time.Time.
+// See https://www.rfc-editor.org/rfc/rfc3339 for the definition
+
+import dt "core:time/datetime"
+
+// Parses an RFC 3339 string and returns Time in UTC, with any UTC offset applied to it.
+// Only 4-digit years are accepted.
+// Optional pointer to boolean `is_leap` will return `true` if the moment was a leap second.
+// Leap seconds are smeared into 23:59:59.
+rfc3339_to_time_utc :: proc(rfc_datetime: string, is_leap: ^bool = nil) -> (res: Time, consumed: int) {
+	offset: int
+
+	res, offset, consumed = rfc3339_to_time_and_offset(rfc_datetime, is_leap)
+	res._nsec += (i64(-offset) * i64(Minute))
+	return res, consumed
+}
+
+// Parses an RFC 3339 string and returns Time and a UTC offset in minutes.
+// e.g. 1985-04-12T23:20:50.52Z
+// Note: Only 4-digit years are accepted.
+// Optional pointer to boolean `is_leap` will return `true` if the moment was a leap second.
+// Leap seconds are smeared into 23:59:59.
+rfc3339_to_time_and_offset :: proc(rfc_datetime: string, is_leap: ^bool = nil) -> (res: Time, utc_offset: int, consumed: int) {
+	moment, offset, leap_second, count := rfc3339_to_components(rfc_datetime)
+	if count == 0 {
+		return
+	}
+
+	if is_leap != nil {
+		is_leap^ = leap_second
+	}
+
+	if _res, ok := datetime_to_time(moment.year, moment.month, moment.day, moment.hour, moment.minute, moment.second, moment.nano); !ok {
+		return {}, 0, 0
+	} else {
+		return _res, offset, count
+	}
+}
+
+// Parses an RFC 3339 string and returns Time and a UTC offset in minutes.
+// e.g. 1985-04-12T23:20:50.52Z
+// Performs no validation on whether components are valid, e.g. it'll return hour = 25 if that's what it's given
+rfc3339_to_components :: proc(rfc_datetime: string) -> (res: dt.DateTime, utc_offset: int, is_leap: bool, consumed: int) {
+	moment, offset, count, leap_second, ok := _rfc3339_to_components(rfc_datetime)
+	if !ok {
+		return
+	}
+	return moment, offset, leap_second, count
+}
+
+// Parses an RFC 3339 string and returns datetime.DateTime.
+// Performs no validation on whether components are valid, e.g. it'll return hour = 25 if that's what it's given
+@(private)
+_rfc3339_to_components :: proc(rfc_datetime: string) -> (res: dt.DateTime, utc_offset: int, consumed: int, is_leap: bool, ok: bool) {
+	// A compliant date is at minimum 20 characters long, e.g. YYYY-MM-DDThh:mm:ssZ
+	(len(rfc_datetime) >= 20) or_return
+
+	// Scan and eat YYYY-MM-DD[Tt], then scan and eat HH:MM:SS, leave separator
+	year   := scan_digits(rfc_datetime[0:], "-",  4) or_return
+	month  := scan_digits(rfc_datetime[5:], "-",  2) or_return
+	day    := scan_digits(rfc_datetime[8:], "Tt", 2) or_return
+	hour   := scan_digits(rfc_datetime[11:], ":", 2) or_return
+	minute := scan_digits(rfc_datetime[14:], ":", 2) or_return
+	second := scan_digits(rfc_datetime[17:], "",  2) or_return
+	nanos  := 0
+	count  := 19
+
+	if rfc_datetime[count] == '.' {
+		// Scan hundredths. The string must be at least 4 bytes long (.hhZ)
+		(len(rfc_datetime[count:]) >= 4) or_return
+		hundredths := scan_digits(rfc_datetime[count+1:], "", 2) or_return
+		count += 3
+		nanos = 10_000_000 * hundredths
+	}
+
+	// Leap second handling
+	if minute == 59 && second == 60 {
+		second = 59
+		is_leap = true
+	}
+
+	err: dt.Error
+	if res, err = dt.components_to_datetime(year, month, day, hour, minute, second, nanos); err != .None {
+		return {}, 0, 0, false, false
+	}
+
+	// Scan UTC offset
+	switch rfc_datetime[count] {
+	case 'Z':
+		utc_offset = 0
+		count += 1
+	case '+', '-':
+		(len(rfc_datetime[count:]) >= 6) or_return
+		offset_hour   := scan_digits(rfc_datetime[count+1:], ":", 2) or_return
+		offset_minute := scan_digits(rfc_datetime[count+4:], "",  2) or_return
+
+		utc_offset = 60 * offset_hour + offset_minute
+		utc_offset *= -1 if rfc_datetime[count] == '-' else 1
+		count += 6
+	}
+	return res, utc_offset, count, is_leap, true
+}
+
+@(private)
+scan_digits :: proc(s: string, sep: string, count: int) -> (res: int, ok: bool) {
+	needed := count + min(1, len(sep))
+	(len(s) >= needed) or_return
+
+	#no_bounds_check for i in 0..<count {
+		if v := s[i]; v >= '0' && v <= '9' {
+			res = res * 10 + int(v - '0')
+		} else {
+			return 0, false
+		}
+	}
+	found_sep := len(sep) == 0
+	#no_bounds_check for v in sep {
+		found_sep |= rune(s[count]) == v
+	}
+	return res, found_sep
+}

+ 24 - 49
core/time/time.odin

@@ -1,6 +1,7 @@
 package time
 package time
 
 
-import "base:intrinsics"
+import    "base:intrinsics"
+import dt "core:time/datetime"
 
 
 Duration :: distinct i64
 Duration :: distinct i64
 
 
@@ -299,10 +300,6 @@ _time_abs :: proc "contextless" (t: Time) -> u64 {
 
 
 @(private)
 @(private)
 _abs_date :: proc "contextless" (abs: u64, full: bool) -> (year: int, month: Month, day: int, yday: int) {
 _abs_date :: proc "contextless" (abs: u64, full: bool) -> (year: int, month: Month, day: int, yday: int) {
-	_is_leap_year :: proc "contextless" (year: int) -> bool {
-		return year%4 == 0 && (year%100 != 0 || year%400 == 0)
-	}
-
 	d := abs / SECONDS_PER_DAY
 	d := abs / SECONDS_PER_DAY
 
 
 	// 400 year cycles
 	// 400 year cycles
@@ -335,7 +332,7 @@ _abs_date :: proc "contextless" (abs: u64, full: bool) -> (year: int, month: Mon
 
 
 	day = yday
 	day = yday
 
 
-	if _is_leap_year(year) {
+	if is_leap_year(year) {
 		switch {
 		switch {
 		case day > 31+29-1:
 		case day > 31+29-1:
 			day -= 1
 			day -= 1
@@ -360,57 +357,35 @@ _abs_date :: proc "contextless" (abs: u64, full: bool) -> (year: int, month: Mon
 	return
 	return
 }
 }
 
 
-datetime_to_time :: proc "contextless" (year, month, day, hour, minute, second: int, nsec := int(0)) -> (t: Time, ok: bool) {
-	divmod :: proc "contextless" (year: int, divisor: int) -> (div: int, mod: int) {
-		if divisor <= 0 {
-			intrinsics.debug_trap()
-		}
-		div = int(year / divisor)
-		mod = year % divisor
+components_to_time :: proc "contextless" (#any_int year, #any_int month, #any_int day, #any_int hour, #any_int minute, #any_int second: i64, #any_int nsec := i64(0)) -> (t: Time, ok: bool) {
+	this_date, err := dt.components_to_datetime(year, month, day, hour, minute, second, nsec)
+	if err != .None {
 		return
 		return
 	}
 	}
-	_is_leap_year :: proc "contextless" (year: int) -> bool {
-		return year%4 == 0 && (year%100 != 0 || year%400 == 0)
-	}
-
-
-	ok = true
-
-	_y := year  - 1970
-	_m := month - 1
-	_d := day   - 1
-
-	if month < 1 || month > 12 {
-		_m %= 12; ok = false
-	}
-	if day   < 1 || day   > 31 {
-		_d %= 31; ok = false
-	}
-
-	s := i64(0)
-	div, mod := divmod(_y, 400)
-	days := div * DAYS_PER_400_YEARS
-
-	div, mod = divmod(mod, 100)
-	days += div * DAYS_PER_100_YEARS
+	return compound_to_time(this_date)
+}
 
 
-	div, mod = divmod(mod, 4)
-	days += (div * DAYS_PER_4_YEARS) + (mod * 365)
+compound_to_time :: proc "contextless" (datetime: dt.DateTime) -> (t: Time, ok: bool) {
+	unix_epoch := dt.DateTime{{1970, 1, 1}, {0, 0, 0, 0}}
+	delta, err := dt.sub(datetime, unix_epoch)
+	ok = err == .None
 
 
-	days += int(days_before[_m]) + _d
+	seconds     := delta.days    * 86_400 + delta.seconds
+	nanoseconds := i128(seconds) * 1e9    + i128(delta.nanos)
 
 
-	if _is_leap_year(year) && _m >= 2 {
-		days += 1
+	// Can this moment be represented in i64 worth of nanoseconds?
+	// min(Time): 1677-09-21 00:12:44.145224192 +0000 UTC
+	// max(Time): 2262-04-11 23:47:16.854775807 +0000 UTC
+	if nanoseconds < i128(min(i64)) || nanoseconds > i128(max(i64)) {
+		return {}, false
 	}
 	}
+	return Time{_nsec=i64(nanoseconds)}, true
+}
 
 
-	s += i64(days)   * SECONDS_PER_DAY
-	s += i64(hour)   * SECONDS_PER_HOUR
-	s += i64(minute) * SECONDS_PER_MINUTE
-	s += i64(second)
-
-	t._nsec = (s * 1e9) + i64(nsec)
+datetime_to_time :: proc{components_to_time, compound_to_time}
 
 
-	return
+is_leap_year :: proc "contextless" (year: int) -> (leap: bool) {
+	return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
 }
 }
 
 
 days_before := [?]i32{
 days_before := [?]i32{

+ 2 - 0
examples/all/all_main.odin

@@ -117,6 +117,7 @@ import table            "core:text/table"
 import edit             "core:text/edit"
 import edit             "core:text/edit"
 import thread           "core:thread"
 import thread           "core:thread"
 import time             "core:time"
 import time             "core:time"
+import datetime         "core:time/datetime"
 
 
 import sysinfo          "core:sys/info"
 import sysinfo          "core:sys/info"
 
 
@@ -225,6 +226,7 @@ _ :: table
 _ :: edit
 _ :: edit
 _ :: thread
 _ :: thread
 _ :: time
 _ :: time
+_ :: datetime
 _ :: sysinfo
 _ :: sysinfo
 _ :: unicode
 _ :: unicode
 _ :: utf8
 _ :: utf8

+ 5 - 1
tests/core/Makefile

@@ -24,7 +24,8 @@ all: c_libc_test \
 	 slice_test \
 	 slice_test \
 	 strings_test \
 	 strings_test \
 	 thread_test \
 	 thread_test \
-	 runtime_test
+	 runtime_test \
+	 time_test
 
 
 download_test_assets:
 download_test_assets:
 	$(PYTHON) download_assets.py
 	$(PYTHON) download_assets.py
@@ -94,3 +95,6 @@ thread_test:
 
 
 runtime_test:
 runtime_test:
 	$(ODIN) run runtime $(COMMON) -out:test_core_runtime
 	$(ODIN) run runtime $(COMMON) -out:test_core_runtime
+
+time_test:
+	$(ODIN) run time $(COMMON) -out:test_core_time

+ 5 - 0
tests/core/build.bat

@@ -100,3 +100,8 @@ echo ---
 echo Running core:runtime tests
 echo Running core:runtime tests
 echo ---
 echo ---
 %PATH_TO_ODIN% run runtime %COMMON% %COLLECTION% -out:test_core_runtime.exe || exit /b
 %PATH_TO_ODIN% run runtime %COMMON% %COLLECTION% -out:test_core_runtime.exe || exit /b
+
+echo ---
+echo Running core:time tests
+echo ---
+%PATH_TO_ODIN% run time %COMMON% %COLLECTION% -out:test_core_time.exe || exit /b

+ 178 - 0
tests/core/time/test_core_time.odin

@@ -0,0 +1,178 @@
+package test_core_time
+
+import "core:fmt"
+import "core:mem"
+import "core:os"
+import "core:testing"
+import "core:time"
+import dt "core:time/datetime"
+
+is_leap_year :: time.is_leap_year
+
+TEST_count := 0
+TEST_fail  := 0
+
+when ODIN_TEST {
+	expect       :: testing.expect
+	expect_value :: testing.expect_value
+	log          :: testing.log
+} else {
+	expect  :: proc(t: ^testing.T, condition: bool, message: string, loc := #caller_location) {
+		TEST_count += 1
+		if !condition {
+			TEST_fail += 1
+			fmt.printf("[%v] %v\n", loc, message)
+			return
+		}
+	}
+	log     :: proc(t: ^testing.T, v: any, loc := #caller_location) {
+		fmt.printf("[%v] ", loc)
+		fmt.printf("log: %v\n", v)
+	}
+}
+
+main :: proc() {
+	t := testing.T{}
+
+	track: mem.Tracking_Allocator
+	mem.tracking_allocator_init(&track, context.allocator)
+	defer mem.tracking_allocator_destroy(&track)
+	context.allocator = mem.tracking_allocator(&track)
+
+	test_ordinal_date_roundtrip(&t)
+	test_component_to_time_roundtrip(&t)
+	test_parse_rfc3339_string(&t)
+
+	for _, leak in track.allocation_map {
+		expect(&t, false, fmt.tprintf("%v leaked %m\n", leak.location, leak.size))
+	}
+	for bad_free in track.bad_free_array {
+		expect(&t, false, fmt.tprintf("%v allocation %p was freed badly\n", bad_free.location, bad_free.memory))
+	}
+
+	fmt.printf("%v/%v tests successful.\n", TEST_count - TEST_fail, TEST_count)
+	if TEST_fail > 0 {
+		os.exit(1)
+	}
+}
+
+@test
+test_ordinal_date_roundtrip :: proc(t: ^testing.T) {
+	expect(t, dt.unsafe_ordinal_to_date(dt.unsafe_date_to_ordinal(dt.MIN_DATE)) == dt.MIN_DATE, "Roundtripping MIN_DATE failed.")
+	expect(t, dt.unsafe_date_to_ordinal(dt.unsafe_ordinal_to_date(dt.MIN_ORD))  == dt.MIN_ORD,  "Roundtripping MIN_ORD failed.")
+	expect(t, dt.unsafe_ordinal_to_date(dt.unsafe_date_to_ordinal(dt.MAX_DATE)) == dt.MAX_DATE, "Roundtripping MAX_DATE failed.")
+	expect(t, dt.unsafe_date_to_ordinal(dt.unsafe_ordinal_to_date(dt.MAX_ORD))  == dt.MAX_ORD,  "Roundtripping MAX_ORD failed.")
+}
+
+/*
+	1990-12-31T23:59:60Z
+
+This represents the leap second inserted at the end of 1990.
+
+	1990-12-31T15:59:60-08:00
+
+This represents the same leap second in Pacific Standard Time, 8 hours behind UTC.
+
+	1937-01-01T12:00:27.87+00:20
+
+This represents the same instant of time as noon, January 1, 1937, Netherlands time.
+Standard time in the Netherlands was exactly 19 minutes and 32.13 seconds ahead of UTC by law from 1909-05-01 through 1937-06-30.
+This time zone cannot be represented exactly using the HH:MM format, and this timestamp uses the closest representable UTC offset.
+*/
+RFC3339_Test :: struct{
+	rfc_3339:     string,
+	datetime:     time.Time,
+	apply_offset: bool,
+	utc_offset:   int,
+	consumed:     int,
+	is_leap:      bool,
+}
+
+// These are based on RFC 3339's examples, see https://www.rfc-editor.org/rfc/rfc3339#page-10
+rfc3339_tests :: []RFC3339_Test{
+	// This represents 20 minutes and 50.52 seconds after the 23rd hour of April 12th, 1985 in UTC.
+	{"1985-04-12T23:20:50.52Z",      {482196050520000000},  true,  0,    23, false},
+
+	// This represents 39 minutes and 57 seconds after the 16th hour of December 19th, 1996 with an offset of -08:00 from UTC (Pacific Standard Time).
+	// Note that this is equivalent to 1996-12-20T00:39:57Z in UTC.
+	{"1996-12-19T16:39:57-08:00",    {851013597000000000},  false, -480, 25, false},
+	{"1996-12-19T16:39:57-08:00",    {851042397000000000},  true,  0,    25, false},
+	{"1996-12-20T00:39:57Z",         {851042397000000000},  false, 0,    20, false},
+
+	// This represents the leap second inserted at the end of 1990.
+	// It'll be represented as 1990-12-31 23:59:59 UTC after parsing, and `is_leap` will be set to `true`.
+	{"1990-12-31T23:59:60Z",         {662687999000000000},  true,  0,    20, true},
+
+	// This represents the same leap second in Pacific Standard Time, 8 hours behind UTC.
+	{"1990-12-31T15:59:60-08:00",    {662687999000000000},  true,  0,    25, true},
+
+	// This represents the same instant of time as noon, January 1, 1937, Netherlands time.
+	// Standard time in the Netherlands was exactly 19 minutes and 32.13 seconds ahead of UTC by law
+	// from 1909-05-01 through 1937-06-30.  This time zone cannot be represented exactly using the
+	// HH:MM format, and this timestamp uses the closest representable UTC offset.
+	{"1937-01-01T12:00:27.87+00:20", {-1041335972130000000}, false, 20,  28, false},
+	{"1937-01-01T12:00:27.87+00:20", {-1041337172130000000}, true,  0,   28, false},
+}
+
+@test
+test_parse_rfc3339_string :: proc(t: ^testing.T) {
+	for test in rfc3339_tests {
+		is_leap := false
+		if test.apply_offset {
+			res, consumed := time.rfc3339_to_time_utc(test.rfc_3339, &is_leap)
+			msg := fmt.tprintf("[apply offet] Parsing failed: %v -> %v (nsec: %v). Expected %v consumed, got %v", test.rfc_3339, res, res._nsec, test.consumed, consumed)
+			expect(t, test.consumed == consumed, msg)
+
+			if test.consumed == consumed {
+				expect(t, test.datetime == res,     fmt.tprintf("Time didn't match. Expected %v (%v), got %v (%v)", test.datetime, test.datetime._nsec, res, res._nsec))
+				expect(t, test.is_leap  == is_leap, "Expected a leap second, got none.")
+			}
+		} else {
+			res, offset, consumed := time.rfc3339_to_time_and_offset(test.rfc_3339)
+			msg := fmt.tprintf("Parsing failed: %v -> %v (nsec: %v), offset: %v. Expected %v consumed, got %v", test.rfc_3339, res, res._nsec, offset, test.consumed, consumed)
+			expect(t, test.consumed == consumed, msg)
+
+			if test.consumed == consumed {
+				expect(t, test.datetime   == res,     fmt.tprintf("Time didn't match. Expected %v (%v), got %v (%v)", test.datetime, test.datetime._nsec, res, res._nsec))
+				expect(t, test.utc_offset == offset,  fmt.tprintf("UTC offset didn't match. Expected %v, got %v", test.utc_offset, offset))
+				expect(t, test.is_leap    == is_leap, "Expected a leap second, got none.")
+			}
+		}
+	}
+}
+
+MONTH_DAYS := []int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
+YEAR_START :: 1900
+YEAR_END   :: 2024
+
+@test
+test_component_to_time_roundtrip :: proc(t: ^testing.T) {
+	// Roundtrip a datetime through `datetime_to_time` to `Time` and back to its components.
+	for year in YEAR_START..=YEAR_END {
+		for month in 1..=12 {
+			days := MONTH_DAYS[month - 1]
+			if month == 2 && is_leap_year(year) {
+				days += 1
+			}
+			for day in 1..=days {
+				d, _ := dt.components_to_datetime(year, month, day, 0, 0, 0, 0)
+				date_component_roundtrip_test(t, d)
+			}
+		}
+	}
+}
+
+date_component_roundtrip_test :: proc(t: ^testing.T, moment: dt.DateTime) {
+	res, ok := time.datetime_to_time(moment.year, moment.month, moment.day, moment.hour, moment.minute, moment.second)
+	expect(t, ok, "Couldn't convert date components into date")
+
+	YYYY, MM, DD := time.date(res)
+	hh,   mm, ss := time.clock(res)
+
+	expected := fmt.tprintf("Expected %4d-%2d-%2d %2d:%2d:%2d, got %4d-%2d-%2d %2d:%2d:%2d",
+	                        moment.year, moment.month, moment.day, moment.hour, moment.minute, moment.second, YYYY, MM, DD, hh, mm, ss)
+
+	ok =  moment.year == i64(YYYY) && moment.month == i8(MM) && moment.day    == i8(DD)
+	ok &= moment.hour == i8(hh)   && moment.minute == i8(mm) && moment.second == i8(ss)
+	expect(t, ok, expected)
+}