///|
/// A calendar moment expressed in a local clock (the UTC offset is supplied separately).
pub struct Moment {
year : Int32
month : Int32
day : Int32
local_hour : Double
} derive(Eq, Debug)
///|
fn pad2(n : Int32) -> String {
if n < 10 {
"0\{n}"
} else {
n.to_string()
}
}
///|
fn parse_clock(s : String) -> Double {
let pieces = s.split(":").filter(v => !v.is_empty()).to_array()
if pieces.length() == 0 {
return 0.0
}
let h = @string.parse_int(pieces[0]) catch { _ => 0 }
let m = if pieces.length() >= 2 {
@string.parse_int(pieces[1]) catch {
_ => 0
}
} else {
0
}
h.to_double() + m.to_double() / 60.0
}
///|
/// Parse "YYYY-MM-DD" or "YYYY-MM-DD HH:MM" (the "T" separator is also accepted).
pub fn parse_local_datetime(s : String) -> Moment? {
let normalized = s.trim().to_owned().replace_all(old="T", new=" ")
let pieces = normalized
.split(" ")
.filter(v => !v.is_empty())
.map(v => v.to_owned())
.to_array()
if pieces.length() == 0 {
return None
}
let date_parts = pieces[0].split("-").to_array()
if date_parts.length() != 3 {
return None
}
let year = @string.parse_int(date_parts[0]) catch { _ => return None }
let month = @string.parse_int(date_parts[1]) catch { _ => return None }
let day = @string.parse_int(date_parts[2]) catch { _ => return None }
if month < 1 || month > 12 || day < 1 || day > 31 {
return None
}
let hour = if pieces.length() >= 2 { parse_clock(pieces[1]) } else { 0.0 }
if hour < 0.0 || hour >= 24.0 {
return None
}
Some({ year, month, day, local_hour: hour, })
}
///|
/// Julian day from a Gregorian date and a UTC hour (Meeus, "Astronomical Algorithms").
pub fn julian_day_utc(
year : Int32,
month : Int32,
day : Int32,
utc_hour : Double,
) -> Double {
let y = if month <= 2 { (year - 1).to_double() } else { year.to_double() }
let m = if month <= 2 { (month + 12).to_double() } else { month.to_double() }
let a = @math.floor(y / 100.0)
let b = 2.0 - a + @math.floor(a / 4.0)
@math.floor(365.25 * (y + 4716.0)) +
@math.floor(30.6001 * (m + 1.0)) +
day.to_double() +
b -
1524.5 +
utc_hour / 24.0
}
///|
/// Convert a local clock moment to a UTC Julian day.
pub fn moment_to_jd(moment : Moment, tz_offset : Double) -> Double {
julian_day_utc(
moment.year,
moment.month,
moment.day,
moment.local_hour - tz_offset,
)
}
///|
/// Inverse of `julian_day_utc`: returns `(year, month, day, hour)` in UTC.
pub fn jd_to_calendar(jd : Double) -> (Int32, Int32, Int32, Double) {
let z = @math.floor(jd + 0.5).to_int()
let f = jd + 0.5 - @math.floor(jd + 0.5)
let a = if z < 2299161 {
z
} else {
let alpha = @math.floor((z.to_double() - 1867216.25) / 36524.25).to_int()
z + 1 + alpha - alpha / 4
}
let b = a + 1524
let c = @math.floor((b.to_double() - 122.1) / 365.25).to_int()
let d = @math.floor(365.25 * c.to_double()).to_int()
let e = @math.floor((b.to_double() - d.to_double()) / 30.6001).to_int()
let day_frac = b.to_double() -
d.to_double() -
@math.floor(30.6001 * e.to_double()) +
f
let day = @math.floor(day_frac).to_int()
let month = if e < 14 { e - 1 } else { e - 13 }
let year = if month > 2 { c - 4716 } else { c - 4715 }
let hour = (day_frac - day.to_double()) * 24.0
(year, month, day, hour)
}
///|
/// Local hour-of-day `[0, 24)` for a Julian day and UTC offset.
pub fn local_hour_of(jd : Double, tz_offset : Double) -> Double {
let loc = jd + tz_offset / 24.0 + 0.5
(loc - @math.floor(loc)) * 24.0
}
///|
/// Format a Julian day as "YYYY-MM-DD HH:MM" in the given UTC offset.
pub fn format_local_datetime(jd : Double, tz_offset : Double) -> String {
let (y, m, d, h) = jd_to_calendar(jd + tz_offset / 24.0)
let hh = @math.floor(h).to_int()
let mm = @math.floor((h - hh.to_double()) * 60.0).to_int()
"\{pad2(y)}-\{pad2(m)}-\{pad2(d)} \{pad2(hh)}:\{pad2(mm)}"
}
///|
/// Format a Julian day as "HH:MM" in the given UTC offset.
pub fn format_local_time(jd : Double, tz_offset : Double) -> String {
let h = local_hour_of(jd, tz_offset)
let hh = @math.floor(h).to_int()
let mm = @math.floor((h - hh.to_double()) * 60.0).to_int()
"\{pad2(hh)}:\{pad2(mm)}"
}
///|
fn is_leap(year : Int32) -> Bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
///|
fn days_in_month(year : Int32, month : Int32) -> Int32 {
match month {
1 => 31
2 => if is_leap(year) { 29 } else { 28 }
3 => 31
4 => 30
5 => 31
6 => 30
7 => 31
8 => 31
9 => 30
10 => 31
11 => 30
12 => 31
_ => 0
}
}
///|
/// Day of year, `1..365/366`.
pub fn day_of_year(year : Int32, month : Int32, day : Int32) -> Int32 {
let mut n = day
for m = 1; m < month; m = m + 1 {
n = n + days_in_month(year, m)
}
n
}