use crate::scalar::to_i64_t;
use crate::util::{fast_digit_parse, le_u64};
use std::cmp::Ordering;
use std::convert::TryFrom;
use std::fmt::{self, Debug, Display};
use std::str::FromStr;
#[derive(Debug, PartialEq, Eq)]
pub struct DateError;
impl std::error::Error for DateError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
impl std::fmt::Display for DateError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "unable to decode date")
}
}
const DAYS_PER_MONTH: [u8; 13] = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
pub trait PdsDate {
fn year(&self) -> i16;
fn month(&self) -> u8;
fn day(&self) -> u8;
fn game_fmt(&self) -> PdsDateFormatter;
fn iso_8601(&self) -> PdsDateFormatter;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DateFormat {
Iso8601,
DotShort,
DotWide,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PdsDateFormatter {
raw: RawDate,
format: DateFormat,
}
impl PdsDateFormatter {
pub fn new(raw: RawDate, format: DateFormat) -> Self {
Self { raw, format }
}
}
impl Display for PdsDateFormatter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.format == DateFormat::Iso8601 {
write!(
f,
"{:04}-{:02}-{:02}",
self.raw.year(),
self.raw.month(),
self.raw.day(),
)?;
if self.raw.has_hour() {
write!(f, "T{:02}", self.raw.hour() - 1)
} else {
Ok(())
}
} else {
let fmt = self.format;
let width = if fmt == DateFormat::DotWide { 2 } else { 0 };
write!(
f,
"{}.{:03$}.{:03$}",
self.raw.year(),
self.raw.month(),
self.raw.day(),
width,
)?;
if self.raw.has_hour() {
write!(f, ".{:01$}", self.raw.hour(), width)
} else {
Ok(())
}
}
}
}
#[derive(Debug)]
struct ExpandedRawDate {
year: i16,
month: u8,
day: u8,
hour: u8,
}
impl ExpandedRawDate {
#[inline]
fn from_binary(mut s: i32) -> Option<Self> {
let hour = s % 24;
s /= 24;
let days_since_jan1 = s % 365;
if hour < 0 || days_since_jan1 < 0 {
return None;
}
s /= 365;
let year = s.checked_sub(5000).and_then(|x| i16::try_from(x).ok())?;
let (month, day) = month_day_from_julian(days_since_jan1);
Some(ExpandedRawDate {
year,
month,
day,
hour: hour as u8,
})
}
#[inline]
fn parse<T: AsRef<[u8]>>(s: T) -> Option<Self> {
Self::_parse(s.as_ref())
}
#[inline]
fn _parse(data: &[u8]) -> Option<Self> {
let (year, data) = to_i64_t(data).ok()?;
if data.is_empty() {
return i32::try_from(year).ok().and_then(Self::from_binary);
}
let year = i16::try_from(year).ok()?;
if *data.first()? != b'.' {
return None;
}
let n = data.get(1)?;
let month1 = if !n.is_ascii_digit() {
return None;
} else {
n - b'0'
};
let n = data.get(2)?;
let (month, offset) = if *n == b'.' {
(month1, 2)
} else if n.is_ascii_digit() {
(month1 * 10 + (n - b'0'), 3)
} else {
return None;
};
if *data.get(offset)? != b'.' {
return None;
}
let n = data.get(offset + 1)?;
let day1 = if !n.is_ascii_digit() {
return None;
} else {
n - b'0'
};
let (day, offset) = match data.get(offset + 2) {
None => {
return Some(ExpandedRawDate {
year,
month,
day: day1,
hour: 0,
});
}
Some(b'.') => (day1, offset + 2),
Some(n) if n.is_ascii_digit() => {
let result = day1 * 10 + (n - b'0');
if data.len() != offset + 3 {
(result, offset + 3)
} else {
return Some(ExpandedRawDate {
year,
month,
day: result,
hour: 0,
});
}
}
_ => return None,
};
if *data.get(offset)? != b'.' {
return None;
}
let n = data.get(offset + 1)?;
let hour1 = if !n.is_ascii_digit() || *n == b'0' {
return None;
} else {
n - b'0'
};
match data.get(offset + 2) {
None => Some(ExpandedRawDate {
year,
month,
day,
hour: hour1,
}),
Some(n) if n.is_ascii_digit() => {
let result = hour1 * 10 + (n - b'0');
if data.len() != offset + 3 {
None
} else {
Some(ExpandedRawDate {
year,
month,
day,
hour: result,
})
}
}
_ => None,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct RawDate {
year: i16,
data: u16,
}
impl Debug for RawDate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"RawDate {{ year: {} month: {} day: {} hour: {} }}",
self.year(),
self.month(),
self.day(),
self.hour()
)
}
}
impl PartialOrd for RawDate {
#[inline]
fn partial_cmp(&self, other: &RawDate) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for RawDate {
#[inline]
fn cmp(&self, other: &RawDate) -> Ordering {
self.year()
.cmp(&other.year())
.then_with(|| self.data.cmp(&other.data))
}
}
impl RawDate {
#[inline]
fn from_expanded(data: ExpandedRawDate) -> Option<Self> {
Self::from_ymdh_opt(data.year, data.month, data.day, data.hour)
}
#[inline]
pub fn from_binary(s: i32) -> Option<Self> {
ExpandedRawDate::from_binary(s).and_then(Self::from_expanded)
}
#[inline]
pub fn from_ymdh_opt(year: i16, month: u8, day: u8, hour: u8) -> Option<Self> {
if month != 0 && month < 13 && day != 0 && day < 32 && hour < 25 {
let data = (u16::from(month) << 12) + (u16::from(day) << 7) + (u16::from(hour) << 2);
Some(RawDate { year, data })
} else {
None
}
}
#[inline]
pub fn from_ymdh(year: i16, month: u8, day: u8, hour: u8) -> Self {
Self::from_ymdh_opt(year, month, day, hour).unwrap()
}
#[inline]
pub fn hour(&self) -> u8 {
((self.data >> 2) & 0x1f) as u8
}
#[inline]
pub fn has_hour(&self) -> bool {
self.data & 0x7c != 0
}
#[inline]
pub fn parse<T: AsRef<[u8]>>(s: T) -> Result<Self, DateError> {
Self::_parse(s.as_ref())
}
#[inline]
fn _parse(s: &[u8]) -> Result<Self, DateError> {
ExpandedRawDate::parse(s)
.and_then(Self::from_expanded)
.and_then(|x| {
if to_i64_t(s).ok()?.1.is_empty() {
None
} else {
Some(x)
}
})
.ok_or(DateError)
}
}
impl PdsDate for RawDate {
#[inline]
fn year(&self) -> i16 {
self.year
}
#[inline]
fn month(&self) -> u8 {
(self.data >> 12) as u8
}
#[inline]
fn day(&self) -> u8 {
((self.data >> 7) & 0x1f) as u8
}
fn game_fmt(&self) -> PdsDateFormatter {
PdsDateFormatter::new(*self, DateFormat::DotShort)
}
fn iso_8601(&self) -> PdsDateFormatter {
PdsDateFormatter::new(*self, DateFormat::Iso8601)
}
}
impl FromStr for RawDate {
type Err = DateError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s.as_bytes())
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Date {
raw: RawDate,
}
impl Debug for Date {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Date {}", self.game_fmt())
}
}
impl Date {
#[inline]
fn from_expanded(date: ExpandedRawDate) -> Option<Self> {
if date.hour != 0 {
None
} else {
Self::from_ymd_opt(date.year, date.month, date.day)
}
}
#[inline]
fn days(&self) -> i32 {
let month_days = julian_ordinal_day(self.month());
let year_day = i32::from(self.year()) * 365;
if year_day < 0 {
year_day - month_days - i32::from(self.day())
} else {
year_day + month_days + i32::from(self.day())
}
}
#[inline]
pub fn from_ymd_opt(year: i16, month: u8, day: u8) -> Option<Self> {
RawDate::from_ymdh_opt(year, month, day, 0).and_then(|raw| {
let days = DAYS_PER_MONTH[usize::from(month)];
if day <= days {
Some(Date { raw })
} else {
None
}
})
}
#[inline]
pub fn from_ymd(year: i16, month: u8, day: u8) -> Self {
Self::from_ymd_opt(year, month, day).unwrap()
}
#[inline]
pub fn parse<T: AsRef<[u8]>>(s: T) -> Result<Self, DateError> {
Self::_parse(s.as_ref())
}
#[inline]
fn fast_parse(r: [u8; 8]) -> Option<Result<Self, DateError>> {
Self::fast_parse_u64(u64::from_le_bytes(r))
}
#[inline]
fn fast_parse_u64(r: u64) -> Option<Result<Self, DateError>> {
let val = fast_digit_parse(r)?;
let day = val % 100;
let month = (val / 100) % 100;
let val = val / 10_000;
let result = Self::from_expanded(ExpandedRawDate {
year: val as i16,
month: month as u8,
day: day as u8,
hour: 0,
})
.ok_or(DateError);
Some(result)
}
#[cold]
fn fallback(s: &[u8]) -> Result<Self, DateError> {
ExpandedRawDate::parse(s)
.and_then(Self::from_expanded)
.ok_or(DateError)
}
#[inline]
fn _parse(s: &[u8]) -> Result<Self, DateError> {
match s {
[y1, y2, y3, y4, b'.', m1, m2, b'.', d1, d2] => {
let r = [*y1, *y2, *y3, *y4, *m1, *m2, *d1, *d2];
Self::fast_parse(r).unwrap_or_else(|| Self::fallback(s))
}
[y1, y2, y3, y4, b'.', m1, m2, b'.', d1] => {
let r = [*y1, *y2, *y3, *y4, *m1, *m2, b'0', *d1];
Self::fast_parse(r).unwrap_or_else(|| Self::fallback(s))
}
[y1, y2, y3, y4, b'.', m1, b'.', d1, d2] => {
let r = [*y1, *y2, *y3, *y4, b'0', *m1, *d1, *d2];
Self::fast_parse(r).unwrap_or_else(|| Self::fallback(s))
}
_ => {
if s.len() == 8 {
let d = le_u64(s);
let one_digit_month = d & 0x00FF_00FF_0000_0000 == 0x002E_002E_0000_0000;
let e = (d & 0xFF30_FF30_FFFF_FFFF) | 0x0030_0030_0000_0000;
if one_digit_month && let Some(x) = Self::fast_parse_u64(e) {
return x;
}
Self::fallback(s)
} else if s.len() < 5 || s.len() > 12 || !matches!(s[0], b'-' | b'0'..=b'9') {
Err(DateError)
} else {
Self::fallback(s)
}
}
}
}
#[inline]
pub fn days_until(self, other: &Date) -> i32 {
other.days() - self.days()
}
#[inline]
pub fn add_days(self, days: i32) -> Date {
let new_days = self
.days()
.checked_add(days)
.expect("adding days overflowed");
let days_since_jan1 = (new_days % 365).abs();
let year = new_days / 365;
let (month, day) = month_day_from_julian(days_since_jan1);
let year = i16::try_from(year).expect("year to fit inside signed 16bits");
Date {
raw: RawDate::from_ymdh(year, month, day, self.raw.hour()),
}
}
#[inline]
pub fn from_binary(s: i32) -> Option<Self> {
ExpandedRawDate::from_binary(s)
.map(|x| ExpandedRawDate { hour: 0, ..x })
.and_then(Self::from_expanded)
}
#[inline]
pub fn from_binary_heuristic(s: i32) -> Option<Self> {
ExpandedRawDate::from_binary(s).and_then(|x| {
if x.year > -100 {
Self::from_expanded(x)
} else {
None
}
})
}
#[inline]
pub fn to_binary(self) -> i32 {
let ordinal_day = julian_ordinal_day(self.month()) + i32::from(self.day());
to_binary(self.year(), ordinal_day, 0)
}
}
impl PdsDate for Date {
#[inline]
fn year(&self) -> i16 {
self.raw.year()
}
#[inline]
fn month(&self) -> u8 {
self.raw.month()
}
#[inline]
fn day(&self) -> u8 {
self.raw.day()
}
fn iso_8601(&self) -> PdsDateFormatter {
PdsDateFormatter::new(self.raw, DateFormat::Iso8601)
}
fn game_fmt(&self) -> PdsDateFormatter {
PdsDateFormatter::new(self.raw, DateFormat::DotShort)
}
}
impl FromStr for Date {
type Err = DateError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s.as_bytes())
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DateHour {
raw: RawDate,
}
impl Debug for DateHour {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "DateHour {}", self.game_fmt())
}
}
impl DateHour {
#[inline]
fn from_expanded(date: ExpandedRawDate) -> Option<Self> {
Self::from_ymdh_opt(date.year, date.month, date.day, date.hour)
}
#[inline]
pub fn from_ymdh_opt(year: i16, month: u8, day: u8, hour: u8) -> Option<Self> {
RawDate::from_ymdh_opt(year, month, day, hour).and_then(|raw| {
let days = DAYS_PER_MONTH[usize::from(month)];
if hour > 0 && day <= days {
Some(Self { raw })
} else {
None
}
})
}
#[inline]
pub fn from_ymdh(year: i16, month: u8, day: u8, hour: u8) -> Self {
Self::from_ymdh_opt(year, month, day, hour).unwrap()
}
pub fn hour(&self) -> u8 {
self.raw.hour()
}
#[inline]
pub fn parse<T: AsRef<[u8]>>(s: T) -> Result<Self, DateError> {
ExpandedRawDate::parse(s)
.and_then(Self::from_expanded)
.ok_or(DateError)
}
#[inline]
pub fn from_binary(s: i32) -> Option<Self> {
ExpandedRawDate::from_binary(s).and_then(|mut raw| {
raw.hour += 1;
Self::from_expanded(raw)
})
}
#[inline]
pub fn from_binary_heuristic(s: i32) -> Option<Self> {
Self::from_binary(s).and_then(|x| {
let is_min_year = x.year() == 1 || x.year() == -1;
let is_min_date = is_min_year && x.month() == 1 && x.day() == 1 && x.hour() == 1;
if x.year() < 1800 && !is_min_date {
None
} else {
Some(x)
}
})
}
#[inline]
pub fn to_binary(self) -> i32 {
let ordinal_day = julian_ordinal_day(self.month()) + i32::from(self.day());
to_binary(self.year(), ordinal_day, self.hour())
}
}
impl PdsDate for DateHour {
#[inline]
fn year(&self) -> i16 {
self.raw.year()
}
#[inline]
fn month(&self) -> u8 {
self.raw.month()
}
#[inline]
fn day(&self) -> u8 {
self.raw.day()
}
fn iso_8601(&self) -> PdsDateFormatter {
PdsDateFormatter::new(self.raw, DateFormat::Iso8601)
}
fn game_fmt(&self) -> PdsDateFormatter {
PdsDateFormatter::new(self.raw, DateFormat::DotShort)
}
}
impl FromStr for DateHour {
type Err = DateError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s.as_bytes())
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct UniformDate {
raw: RawDate,
}
impl Debug for UniformDate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "UniformDate {}", self.game_fmt())
}
}
impl UniformDate {
#[inline]
fn from_expanded(date: ExpandedRawDate) -> Option<Self> {
if date.hour != 0 {
None
} else {
Self::from_ymd_opt(date.year, date.month, date.day)
}
}
#[inline]
pub fn from_ymd_opt(year: i16, month: u8, day: u8) -> Option<Self> {
if day > 30 {
None
} else {
RawDate::from_ymdh_opt(year, month, day, 0).map(|raw| Self { raw })
}
}
#[inline]
pub fn from_ymd(year: i16, month: u8, day: u8) -> Self {
Self::from_ymd_opt(year, month, day).unwrap()
}
#[inline]
pub fn parse<T: AsRef<[u8]>>(s: T) -> Result<Self, DateError> {
Self::_parse(s.as_ref())
}
#[inline]
fn _parse(s: &[u8]) -> Result<Self, DateError> {
ExpandedRawDate::parse(s)
.and_then(Self::from_expanded)
.ok_or(DateError)
}
}
impl PdsDate for UniformDate {
#[inline]
fn year(&self) -> i16 {
self.raw.year()
}
#[inline]
fn month(&self) -> u8 {
self.raw.month()
}
#[inline]
fn day(&self) -> u8 {
self.raw.day()
}
fn iso_8601(&self) -> PdsDateFormatter {
PdsDateFormatter::new(self.raw, DateFormat::Iso8601)
}
fn game_fmt(&self) -> PdsDateFormatter {
PdsDateFormatter::new(self.raw, DateFormat::DotWide)
}
}
impl FromStr for UniformDate {
type Err = DateError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s.as_bytes())
}
}
#[inline]
fn month_day_from_julian(days_since_jan1: i32) -> (u8, u8) {
let (month, day) = match days_since_jan1 {
0..=30 => (1, days_since_jan1 + 1),
31..=58 => (2, days_since_jan1 - 30),
59..=89 => (3, days_since_jan1 - 58),
90..=119 => (4, days_since_jan1 - 89),
120..=150 => (5, days_since_jan1 - 119),
151..=180 => (6, days_since_jan1 - 150),
181..=211 => (7, days_since_jan1 - 180),
212..=242 => (8, days_since_jan1 - 211),
243..=272 => (9, days_since_jan1 - 242),
273..=303 => (10, days_since_jan1 - 272),
304..=333 => (11, days_since_jan1 - 303),
334..=364 => (12, days_since_jan1 - 333),
_ => unreachable!(),
};
debug_assert!(day < 255);
(month, day as u8)
}
#[inline]
fn julian_ordinal_day(month: u8) -> i32 {
match month {
1 => -1,
2 => 30,
3 => 58,
4 => 89,
5 => 119,
6 => 150,
7 => 180,
8 => 211,
9 => 242,
10 => 272,
11 => 303,
12 => 333,
_ => unreachable!(),
}
}
#[inline]
fn to_binary(year: i16, ordinal_day: i32, hour: u8) -> i32 {
let year_part = (i32::from(year) + 5000) * 365;
let hour = i32::from(hour.saturating_sub(1));
(year_part + ordinal_day) * 24 + hour
}
#[cfg(feature = "serde")]
mod datederive {
use super::{Date, DateHour, PdsDate, UniformDate};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de, de::Visitor};
use std::fmt;
impl Serialize for Date {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.iso_8601().to_string().as_str())
}
}
struct DateVisitor;
impl Visitor<'_> for DateVisitor {
type Value = Date;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a date")
}
fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E>
where
E: de::Error,
{
Date::from_binary(v)
.ok_or_else(|| de::Error::custom(format!("invalid binary date: {}", v)))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Date::parse(v).map_err(|_e| de::Error::custom(format!("invalid date: {}", v)))
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: de::Error,
{
self.visit_str(v.as_str())
}
}
impl<'de> Deserialize<'de> for Date {
fn deserialize<D>(deserializer: D) -> Result<Date, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(DateVisitor)
}
}
impl Serialize for DateHour {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.iso_8601().to_string().as_str())
}
}
struct DateHourVisitor;
impl Visitor<'_> for DateHourVisitor {
type Value = DateHour;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a date hour")
}
fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E>
where
E: de::Error,
{
DateHour::from_binary(v)
.ok_or_else(|| de::Error::custom(format!("invalid binary date hour: {}", v)))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
DateHour::parse(v).map_err(|_e| de::Error::custom(format!("invalid date hour: {}", v)))
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: de::Error,
{
self.visit_str(v.as_str())
}
}
impl<'de> Deserialize<'de> for DateHour {
fn deserialize<D>(deserializer: D) -> Result<DateHour, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(DateHourVisitor)
}
}
struct UniformDateVisitor;
impl Visitor<'_> for UniformDateVisitor {
type Value = UniformDate;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a uniform date")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
UniformDate::parse(v)
.map_err(|_e| de::Error::custom(format!("invalid uniform date: {}", v)))
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: de::Error,
{
self.visit_str(v.as_str())
}
}
impl<'de> Deserialize<'de> for UniformDate {
fn deserialize<D>(deserializer: D) -> Result<UniformDate, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(UniformDateVisitor)
}
}
}
#[cfg(not(feature = "serde"))]
mod datederive {}
#[cfg(test)]
mod tests {
use super::*;
use quickcheck_macros::quickcheck;
use rstest::*;
#[test]
fn test_date_iso() {
let date = Date::parse("1400.1.2").unwrap();
assert_eq!(date.iso_8601().to_string(), String::from("1400-01-02"));
}
#[test]
fn test_date_parse() {
assert_eq!(Date::parse("1.01.01").unwrap(), Date::from_ymd(1, 1, 1));
}
#[test]
fn test_first_bin_date() {
let date = Date::from_binary(56379360).unwrap();
assert_eq!(date.iso_8601().to_string(), String::from("1436-01-01"));
}
#[test]
fn test_text_date_overflow() {
assert!(Date::parse("1444.257.1").is_err());
assert!(Date::parse("1444.1.257").is_err());
assert!(Date::parse("60000.1.1").is_err());
assert!(Date::parse("-60000.1.1").is_err());
}
#[test]
fn test_binary_date_overflow() {
assert_eq!(Date::from_binary(999379360), None);
}
#[test]
#[should_panic]
fn test_add_adds_year_overflow() {
let date = Date::parse("1400.1.2").unwrap();
let _ = date.add_days(100000000);
}
#[test]
#[should_panic]
fn test_add_adds_day_overflow() {
let date = Date::parse("1400.1.2").unwrap();
let _ = date.add_days(i32::MAX);
}
#[test]
fn test_ignore_bin_dates() {
assert_eq!(Date::from_binary_heuristic(0), None);
assert_eq!(Date::from_binary_heuristic(380947), None);
assert_eq!(Date::from_binary_heuristic(21282204), None);
assert_eq!(Date::from_binary_heuristic(33370842), None);
assert_eq!(Date::from_binary_heuristic(42267422), None);
assert_eq!(Date::from_binary_heuristic(693362154), None);
}
#[test]
fn test_negative_date() {
let date = Date::parse("-17.1.1").unwrap();
assert_eq!(date.game_fmt().to_string(), String::from("-17.1.1"));
let date2 = Date::from_binary(43651080).unwrap();
assert_eq!(date.game_fmt().to_string(), String::from("-17.1.1"));
assert_eq!(date, date2);
}
#[rstest]
#[case("-1.1.1")]
#[case("-1.1.12")]
#[case("-1.11.1")]
#[case("-1.11.12")]
#[case("-10.1.1")]
#[case("-10.1.12")]
#[case("-10.11.1")]
#[case("-10.11.12")]
#[case("-100.1.1")]
#[case("-100.1.12")]
#[case("-100.11.1")]
#[case("-100.11.12")]
#[case("-1000.1.1")]
#[case("-1000.1.12")]
#[case("-1000.11.1")]
#[case("-1000.11.12")]
#[case("1.1.1")]
#[case("1.1.12")]
#[case("1.11.1")]
#[case("1.11.12")]
#[case("10.1.1")]
#[case("10.1.12")]
#[case("10.11.1")]
#[case("10.11.12")]
#[case("100.1.1")]
#[case("100.1.12")]
#[case("100.11.1")]
#[case("100.11.12")]
#[case("1000.1.1")]
#[case("1000.1.12")]
#[case("1000.11.1")]
#[case("1000.11.12")]
#[case("1400.1.2")]
#[case("1457.3.5")]
#[case("1.1.1")]
#[case("1444.11.11")]
#[case("1444.11.30")]
#[case("1444.2.19")]
#[case("1444.12.3")]
fn test_date_game_fmt_roundtrip(#[case] input: &str) {
let s = Date::parse(input).unwrap().game_fmt().to_string();
assert_eq!(&s, input);
}
#[test]
fn test_zero_date() {
let date = Date::from_binary(43800000).unwrap();
assert_eq!(date.iso_8601().to_string(), String::from("0000-01-01"));
}
#[test]
fn test_negative_datehour_binary() {
let date = DateHour::from_binary(43791240).unwrap();
assert_eq!(date.game_fmt().to_string(), String::from("-1.1.1.1"));
assert_eq!(Some(date), DateHour::from_binary_heuristic(43791240));
}
#[test]
fn test_very_negative_date() {
let date = Date::parse("-2500.1.1").unwrap();
assert_eq!(date.game_fmt().to_string(), String::from("-2500.1.1"));
let date2 = Date::from_binary(21900000).unwrap();
assert_eq!(date2.game_fmt().to_string(), String::from("-2500.1.1"));
assert_eq!(date, date2);
}
#[test]
fn test_very_negative_date2() {
let date = Date::parse("-10000.1.1").unwrap();
assert_eq!(date.game_fmt().to_string(), String::from("-10000.1.1"));
let date2 = Date::from_binary(-43800000).unwrap();
assert_eq!(date2.game_fmt().to_string(), String::from("-10000.1.1"));
assert_eq!(date, date2);
}
#[test]
fn test_november_date_regression() {
let date = Date::from_binary(56379360).unwrap().add_days(303);
assert_eq!(date.iso_8601().to_string(), String::from("1436-10-31"));
let date = Date::from_binary(56379360).unwrap().add_days(304);
assert_eq!(date.iso_8601().to_string(), String::from("1436-11-01"));
let date = Date::from_binary(56379360).unwrap().add_days(303 - 30);
assert_eq!(date.iso_8601().to_string(), String::from("1436-10-01"));
let date = Date::from_binary(56379360).unwrap().add_days(303 - 31);
assert_eq!(date.iso_8601().to_string(), String::from("1436-09-30"));
let date = Date::from_binary(56379360).unwrap().add_days(303 - 31 - 29);
assert_eq!(date.iso_8601().to_string(), String::from("1436-09-01"));
let date = Date::from_binary(56379360).unwrap().add_days(303 - 31 - 30);
assert_eq!(date.iso_8601().to_string(), String::from("1436-08-31"));
}
#[test]
fn test_past_leap_year_bin_date() {
let date = Date::from_binary(59611248).unwrap();
assert_eq!(date.iso_8601().to_string(), String::from("1804-12-09"));
}
#[test]
fn test_early_leap_year_bin_date() {
let date = Date::from_binary(57781584).unwrap();
assert_eq!(date.iso_8601().to_string(), String::from("1596-01-27"));
}
#[test]
fn test_non_leap_year_bin_date() {
let date = Date::from_binary(57775944).unwrap();
assert_eq!(date.iso_8601().to_string(), String::from("1595-06-06"));
}
#[test]
fn test_early_date() {
let date = Date::from_binary(43808760).unwrap();
assert_eq!(date.iso_8601().to_string(), String::from("0001-01-01"));
}
#[test]
fn test_days_until() {
let date = Date::parse("1400.1.2").unwrap();
let date2 = Date::parse("1400.1.3").unwrap();
assert_eq!(1, date.days_until(&date2));
}
#[test]
fn test_days_until2() {
let date = Date::parse("1400.1.2").unwrap();
let date2 = Date::parse("1401.1.2").unwrap();
assert_eq!(365, date.days_until(&date2));
}
#[test]
fn test_days_until3() {
let date = Date::parse("1400.1.1").unwrap();
let date2 = Date::parse("1401.12.31").unwrap();
assert_eq!(729, date.days_until(&date2));
}
#[test]
fn test_days_until4() {
let date = Date::parse("1400.1.2").unwrap();
let date2 = Date::parse("1400.1.2").unwrap();
assert_eq!(0, date.days_until(&date2));
}
#[test]
fn test_days_until5() {
let date = Date::parse("1400.1.1").unwrap();
let date2 = Date::parse("1401.12.31").unwrap();
assert_eq!(-729, date2.days_until(&date));
}
#[test]
fn test_add_days() {
let date = Date::parse("1400.1.2").unwrap();
let actual = date.add_days(1);
let expected = Date::parse("1400.1.3").unwrap();
assert_eq!(actual, expected);
}
#[test]
fn test_add_days2() {
let date = Date::parse("1400.1.2").unwrap();
let actual = date.add_days(365);
let expected = Date::parse("1401.1.2").unwrap();
assert_eq!(actual, expected);
}
#[test]
fn test_add_days3() {
let date = Date::parse("1400.1.1").unwrap();
let actual = date.add_days(729);
let expected = Date::parse("1401.12.31").unwrap();
assert_eq!(actual, expected);
}
#[test]
fn test_add_days4() {
let date = Date::parse("1400.1.2").unwrap();
let actual = date.add_days(0);
let expected = Date::parse("1400.1.2").unwrap();
assert_eq!(actual, expected);
}
#[test]
fn test_all_days() {
let start = Date::parse("1400.1.1").unwrap();
for i in 0..364 {
let (month, day) = month_day_from_julian(i);
let next = Date::parse(format!("1400.{}.{}", month, day)).unwrap();
assert_eq!(start.add_days(i), next);
assert_eq!(start.days_until(&next), i);
}
}
#[test]
fn test_cmp() {
let date = Date::parse("1457.3.5").unwrap();
let date2 = Date::parse("1457.3.4").unwrap();
assert!(date2 < date);
}
#[test]
fn test_binary_date_regression() {
let input = i32::from_le_bytes([14, 54, 43, 253]);
let _ = Date::from_binary(input);
}
#[test]
fn test_day_overflow_regression() {
let _ = Date::from_ymd_opt(1222, 12, 222);
}
#[test]
fn test_date_days() {
let date = Date::parse("1.1.1").unwrap();
assert_eq!(date.days(), 365);
let date = Date::parse("-1.1.1").unwrap();
assert_eq!(date.days(), -365);
let date = Date::parse("-1.1.2").unwrap();
assert_eq!(date.days(), -366);
let date = Date::parse("-1.2.2").unwrap();
assert_eq!(date.days(), -397);
}
#[test]
fn test_negative_date_math() {
let date = Date::parse("-1.1.2").unwrap();
let d1 = date.add_days(1);
assert_eq!(d1.game_fmt().to_string(), "-1.1.1");
assert_eq!(date.days_until(&d1), 1);
let date = Date::parse("-3.6.3").unwrap();
let d1 = date.add_days(1);
assert_eq!(d1.game_fmt().to_string(), "-3.6.2");
assert_eq!(date.days_until(&d1), 1);
}
#[test]
fn test_datehour_roundtrip() {
let date = DateHour::parse("1936.1.1.24").unwrap();
assert_eq!(date.iso_8601().to_string(), String::from("1936-01-01T23"));
}
#[test]
fn test_date_zeros_hour() {
let data = i32::from_le_bytes([0x4b, 0x1d, 0x9f, 0x03]);
let date = Date::from_binary(data).unwrap();
let date_hour = DateHour::from_binary(data).unwrap();
assert_eq!(date.iso_8601().to_string(), String::from("1936-01-01"));
assert_eq!(
date_hour.iso_8601().to_string(),
String::from("1936-01-01T11")
);
}
#[test]
fn test_non_zero_binary_hours_are_not_heuristic_dates() {
let data = i32::from_le_bytes([0x4b, 0x1d, 0x9f, 0x03]);
assert_eq!(Date::from_binary_heuristic(data), None);
}
#[test]
fn test_date_disallow_hour_parse_str() {
assert!(Date::parse("1936.1.1.0").is_err())
}
#[test]
fn test_date_state_of_wide_number() {
assert_eq!(Date::parse("1936.01.01"), Ok(Date::from_ymd(1936, 1, 1)));
assert_eq!(
DateHour::parse("1936.01.01.12"),
Ok(DateHour::from_ymdh(1936, 1, 1, 12))
);
}
#[test]
fn test_date_to_binary() {
let date = Date::from_ymd(1, 1, 1);
let bin = date.to_binary();
assert_eq!(Date::from_binary(bin).unwrap(), date);
assert_eq!(Date::from_binary_heuristic(bin).unwrap(), date);
}
#[test]
fn test_date_hour_to_binary() {
let date = DateHour::from_ymdh(1, 1, 1, 1);
let bin = date.to_binary();
assert_eq!(DateHour::from_binary(bin).unwrap(), date);
assert_eq!(DateHour::from_binary_heuristic(bin).unwrap(), date);
assert_eq!(DateHour::from_binary_heuristic(1), None);
}
#[test]
fn test_uniform_date() {
let date = UniformDate::from_ymd(2205, 2, 30);
assert_eq!(date.iso_8601().to_string(), String::from("2205-02-30"));
assert_eq!(date.game_fmt().to_string(), String::from("2205.02.30"));
let date2 = UniformDate::parse("2205.02.30").unwrap();
assert_eq!(date, date2);
let date3 = UniformDate::parse("1.01.01").unwrap();
assert_eq!(date3.game_fmt().to_string(), String::from("1.01.01"));
}
#[test]
fn test_date_converted_into_number() {
assert!(RawDate::parse(b"43808760").is_err());
assert_eq!(Date::parse(b"43808760").unwrap(), Date::from_ymd(1, 1, 1));
}
#[test]
fn test_from_str_impl() {
let _date: RawDate = "1444.11.11".parse().unwrap();
let _date: Date = "1444.11.11".parse().unwrap();
let _date: DateHour = "1936.1.1.1".parse().unwrap();
let _date: UniformDate = "2200.01.01".parse().unwrap();
}
#[test]
fn test_date_hour_negative_regression() {
assert!(Date::from_binary(-1).is_none());
assert!(DateHour::from_binary(-1).is_none());
assert!(Date::from_binary(-24).is_none());
}
#[test]
fn test_date_parse_edge_cases() {
assert!(Date::parse("05.5.3`.3").is_err());
}
#[test]
fn test_memory_size() {
assert!(std::mem::size_of::<Date>() <= 2 * std::mem::size_of::<usize>());
}
#[quickcheck]
fn test_binary_date_equality(data: i32) -> bool {
Date::from_binary(data)
.map(|x| x == Date::from_binary(x.to_binary()).unwrap())
.unwrap_or(true)
}
#[quickcheck]
fn test_binary_date_hour_equality(data: i32) -> bool {
DateHour::from_binary(data)
.map(|x| x == DateHour::from_binary(x.to_binary()).unwrap())
.unwrap_or(true)
}
}