///|
pub(all) struct UtcTime {
hour_value : Int
minute_value : Int
second_value : Int
nanosecond_value : Int
second_wire_value : ExactDecimal
} derive(Eq, Debug)
///|
pub(all) struct UtcDate {
year_value : Int
month_value : Int
day_value : Int
} derive(Eq, Debug)
///|
pub(all) struct UtcDateTime {
date_value : UtcDate
time_value : UtcTime
} derive(Eq, Debug)
///|
fn time_error(source : SourceRef, message : String) -> NmeaError {
NmeaError::from_diagnostic(
Diagnostic::new(TimeInvalid, Error, message, source),
)
}
///|
fn date_error(source : SourceRef, message : String) -> NmeaError {
NmeaError::from_diagnostic(
Diagnostic::new(DateInvalid, Error, message, source),
)
}
///|
fn decimal_pair(
text : String,
start : Int,
source : SourceRef,
) -> Result[Int, NmeaError] {
if start < 0 || start + 1 >= text.length() {
return Err(time_error(source, "time or date pair is missing"))
}
let first = text[start].to_int()
let second = text[start + 1].to_int()
if first < 48 || first > 57 || second < 48 || second > 57 {
return Err(time_error(source, "time or date contains a non-digit"))
}
Ok((first - 48) * 10 + second - 48)
}
///|
fn power10(count : Int) -> Int {
let mut value = 1
let mut index = 0
while index < count {
value = value * 10
index = index + 1
}
value
}
///|
pub fn UtcTime::parse(
text : String,
source : SourceRef,
) -> Result[UtcTime, NmeaError] {
if text.length() < 6 {
return Err(time_error(source, "UTC time requires hhmmss"))
}
if text.length() > 6 && (text.length() == 7 || text[6].to_int() != 46) {
return Err(
time_error(
source, "UTC fractional seconds require a decimal point and digits",
),
)
}
let hour = match decimal_pair(text, 0, source) {
Ok(value) => value
Err(error) => return Err(error)
}
let minute = match decimal_pair(text, 2, source) {
Ok(value) => value
Err(error) => return Err(error)
}
let second = match decimal_pair(text, 4, source) {
Ok(value) => value
Err(error) => return Err(error)
}
if hour > 23 || minute > 59 || second > 60 {
return Err(time_error(source, "UTC time component is outside its range"))
}
let second_text = text[4:].to_owned()
let second_wire = match ExactDecimal::parse(second_text, max_scale=9) {
Ok(value) => value
Err(_) =>
return Err(time_error(source, "UTC seconds are not a valid decimal"))
}
let scale = second_wire.scale()
let whole_coefficient = second.to_int64() * power10(scale).to_int64()
let fractional = second_wire.coefficient() - whole_coefficient
let nanosecond = (fractional * power10(9 - scale).to_int64()).to_int()
Ok({
hour_value: hour,
minute_value: minute,
second_value: second,
nanosecond_value: nanosecond,
second_wire_value: second_wire,
})
}
///|
pub fn UtcTime::hour(self : UtcTime) -> Int {
self.hour_value
}
///|
pub fn UtcTime::minute(self : UtcTime) -> Int {
self.minute_value
}
///|
pub fn UtcTime::second(self : UtcTime) -> Int {
self.second_value
}
///|
pub fn UtcTime::nanosecond(self : UtcTime) -> Int {
self.nanosecond_value
}
///|
fn pad2(value : Int) -> String {
if value < 10 {
"0" + value.to_string()
} else {
value.to_string()
}
}
///|
pub fn UtcTime::to_string(self : UtcTime) -> String {
pad2(self.hour_value) +
":" +
pad2(self.minute_value) +
":" +
self.second_wire_value.to_string()
}
///|
/// Renders the compact hhmmss decimal form used by NMEA fields.
pub fn utc_time_wire(value : UtcTime) -> String {
pad2(value.hour_value) +
pad2(value.minute_value) +
value.second_wire_value.to_string()
}
///|
fn is_leap_year(year : Int) -> Bool {
year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)
}
///|
fn days_in_month(year : Int, month : Int) -> Int {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31
4 | 6 | 9 | 11 => 30
2 => if is_leap_year(year) { 29 } else { 28 }
_ => 0
}
}
///|
pub fn UtcDate::new(
year : Int,
month : Int,
day : Int,
source : SourceRef,
) -> Result[UtcDate, NmeaError] {
if year < 1900 || year > 9999 || month < 1 || month > 12 {
return Err(
date_error(source, "UTC date year or month is outside its range"),
)
}
let maximum = days_in_month(year, month)
if day < 1 || day > maximum {
return Err(date_error(source, "UTC date day is outside its month"))
}
Ok({ year_value: year, month_value: month, day_value: day })
}
///|
pub fn UtcDate::parse_rmc(
text : String,
source : SourceRef,
) -> Result[UtcDate, NmeaError] {
if text.length() != 6 {
return Err(date_error(source, "RMC date requires ddmmyy"))
}
let day = match decimal_pair(text, 0, source) {
Ok(value) => value
Err(_) => return Err(date_error(source, "RMC day is invalid"))
}
let month = match decimal_pair(text, 2, source) {
Ok(value) => value
Err(_) => return Err(date_error(source, "RMC month is invalid"))
}
let short_year = match decimal_pair(text, 4, source) {
Ok(value) => value
Err(_) => return Err(date_error(source, "RMC year is invalid"))
}
let year = if short_year <= 79 {
2000 + short_year
} else {
1900 + short_year
}
UtcDate::new(year, month, day, source)
}
///|
pub fn UtcDate::year(self : UtcDate) -> Int {
self.year_value
}
///|
pub fn UtcDate::month(self : UtcDate) -> Int {
self.month_value
}
///|
pub fn UtcDate::day(self : UtcDate) -> Int {
self.day_value
}
///|
/// Renders the compact ddmmyy form used by RMC.
pub fn utc_date_rmc_wire(value : UtcDate) -> String {
pad2(value.day_value) + pad2(value.month_value) + pad2(value.year_value % 100)
}
///|
fn UtcDate::days_since_epoch(self : UtcDate) -> Int64 {
let mut days : Int64 = 0L
if self.year_value >= 1970 {
let mut year = 1970
while year < self.year_value {
days = days + (if is_leap_year(year) { 366L } else { 365L })
year = year + 1
}
} else {
let mut year = self.year_value
while year < 1970 {
days = days - (if is_leap_year(year) { 366L } else { 365L })
year = year + 1
}
}
let mut month = 1
while month < self.month_value {
days = days + days_in_month(self.year_value, month).to_int64()
month = month + 1
}
days + (self.day_value - 1).to_int64()
}
///|
pub fn UtcDateTime::new(date : UtcDate, time : UtcTime) -> UtcDateTime {
{ date_value: date, time_value: time }
}
///|
pub fn UtcDateTime::date(self : UtcDateTime) -> UtcDate {
self.date_value
}
///|
pub fn UtcDateTime::time(self : UtcDateTime) -> UtcTime {
self.time_value
}
///|
fn UtcDateTime::epoch_nanoseconds(self : UtcDateTime) -> Int64 {
let day_seconds = self.date_value.days_since_epoch() * 86400L
let time_seconds = (self.time_value.hour_value * 3600 +
self.time_value.minute_value * 60 +
self.time_value.second_value).to_int64()
(day_seconds + time_seconds) * 1000000000L +
self.time_value.nanosecond_value.to_int64()
}
///|
pub fn UtcDateTime::millis_since(
self : UtcDateTime,
earlier : UtcDateTime,
) -> Int64 {
(self.epoch_nanoseconds() - earlier.epoch_nanoseconds()) / 1000000L
}
///|
pub fn UtcDateTime::compare(self : UtcDateTime, other : UtcDateTime) -> Int {
let left = self.epoch_nanoseconds()
let right = other.epoch_nanoseconds()
if left < right {
-1
} else if left > right {
1
} else {
0
}
}