use std::str::FromStr;
use crate::errors::{QlError, QlResult};
use crate::time::date::{Date, Month};
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
use crate::types::Integer;
pub fn parse_one_period(s: &str) -> QlResult<Period> {
crate::require!(
s.len() > 1,
"single period require a string of at least 2 characters"
);
let i_pos = match s.find(['D', 'd', 'W', 'w', 'M', 'm', 'Y', 'y']) {
Some(p) if p == s.len() - 1 => p,
_ => {
let last = s.chars().last().expect("s has length > 1");
crate::fail!("unknown '{last}' unit");
}
};
let units = match s.as_bytes()[i_pos].to_ascii_uppercase() {
b'D' => TimeUnit::Days,
b'W' => TimeUnit::Weeks,
b'M' => TimeUnit::Months,
_ => TimeUnit::Years,
};
let n_pos = match s.find(|c: char| c == '-' || c == '+' || c.is_ascii_digit()) {
Some(p) if p < i_pos => p,
_ => crate::fail!("no numbers of {units} provided"),
};
let n = s[n_pos..i_pos].parse::<Integer>().map_err(|_| {
QlError::new(
format!("unable to parse the number of units of {units} in '{s}'"),
file!(),
line!(),
)
})?;
Ok(Period::new(n, units))
}
pub fn parse_period(s: &str) -> QlResult<Period> {
crate::require!(s.len() > 1, "period string length must be at least 2");
let mut segments = Vec::new();
let mut start = 0;
for (i, ch) in s.char_indices() {
if matches!(ch, 'D' | 'd' | 'W' | 'w' | 'M' | 'm' | 'Y' | 'y') {
segments.push(&s[start..=i]);
start = i + 1;
}
}
crate::require!(start == s.len(), "unknown '{s}' unit");
let mut result = parse_one_period(segments[0])?;
for seg in &segments[1..] {
let next = parse_one_period(seg)?;
crate::require!(
is_summable(&result, &next),
"impossible addition between {result} and {next}"
);
result += next;
}
Ok(result)
}
fn is_summable(acc: &Period, incoming: &Period) -> bool {
use TimeUnit::{Days, Months, Weeks, Years};
acc.length() == 0
|| incoming.length() == 0
|| acc.units() == incoming.units()
|| matches!(
(acc.units(), incoming.units()),
(Years, Months) | (Months, Years) | (Weeks, Days) | (Days, Weeks)
)
}
pub fn parse_iso_date(s: &str) -> QlResult<Date> {
let bytes = s.as_bytes();
crate::require!(
s.len() == 10 && bytes[4] == b'-' && bytes[7] == b'-',
"invalid format"
);
let year = s[0..4]
.parse::<Integer>()
.map_err(|_| QlError::new("invalid format", file!(), line!()))?;
let month = s[5..7]
.parse::<Integer>()
.map_err(|_| QlError::new("invalid format", file!(), line!()))?;
let day = s[8..10]
.parse::<Integer>()
.map_err(|_| QlError::new("invalid format", file!(), line!()))?;
crate::require!((1901..=2199).contains(&year), "invalid format");
crate::require!((1..=12).contains(&month), "invalid format");
let month = Month::from_ordinal(month);
let last_day = Date::end_of_month(Date::new(1, month, year)).day_of_month();
crate::require!((1..=last_day).contains(&day), "invalid format");
Ok(Date::new(day, month, year))
}
impl FromStr for Period {
type Err = QlError;
fn from_str(s: &str) -> QlResult<Period> {
parse_period(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_iso_date() {
assert_eq!(
parse_iso_date("2006-01-15").unwrap(),
Date::new(15, Month::January, 2006)
);
}
#[test]
fn iso_date_rejects_bad_input() {
assert!(parse_iso_date("2006-1-15").is_err()); assert!(parse_iso_date("2006/01/15").is_err()); assert!(parse_iso_date("2006-13-15").is_err()); assert!(parse_iso_date("20xx-01-15").is_err()); assert!(parse_iso_date("2006-02-30").is_err()); assert!(parse_iso_date("1900-01-01").is_err()); assert!(parse_iso_date("2006-01-1é").is_err()); }
#[test]
fn parses_single_periods() {
assert_eq!(
parse_period("6M").unwrap(),
Period::new(6, TimeUnit::Months)
);
assert_eq!(parse_period("1Y").unwrap(), Period::new(1, TimeUnit::Years));
assert_eq!(parse_period("2W").unwrap(), Period::new(2, TimeUnit::Weeks));
assert_eq!(
parse_period("-3D").unwrap(),
Period::new(-3, TimeUnit::Days)
);
assert_eq!(parse_period("+3D").unwrap(), Period::new(3, TimeUnit::Days));
}
#[test]
fn parse_period_is_case_insensitive() {
assert_eq!(
parse_period("6m").unwrap(),
Period::new(6, TimeUnit::Months)
);
}
#[test]
fn parses_compound_period() {
let expected = Period::new(2, TimeUnit::Years) + Period::new(6, TimeUnit::Months);
assert_eq!(parse_period("2Y6M").unwrap(), expected);
}
#[test]
fn parse_period_rejects_incompatible_units() {
assert!(parse_period("1D1Y").is_err());
assert!(parse_period("1Y1D").is_err());
assert_eq!(
parse_period("1W2D").unwrap(),
Period::new(1, TimeUnit::Weeks) + Period::new(2, TimeUnit::Days)
);
assert_eq!(
parse_period("2Y6M").unwrap(),
Period::new(2, TimeUnit::Years) + Period::new(6, TimeUnit::Months)
);
}
#[test]
fn parse_period_rejects_bad_input() {
assert!(parse_period("").is_err()); assert!(parse_period("6").is_err()); assert!(parse_period("M").is_err()); assert!(parse_period("6X").is_err()); assert!(parse_period("M6").is_err()); assert!(parse_period("MM").is_err()); assert!(parse_period("66").is_err()); }
#[test]
fn round_trips_through_display() {
let periods = [
Period::new(6, TimeUnit::Months),
Period::new(2, TimeUnit::Weeks),
Period::new(-3, TimeUnit::Days),
Period::new(1, TimeUnit::Years),
Period::new(12, TimeUnit::Months),
];
for p in periods {
assert_eq!(parse_period(&p.to_string()).unwrap(), p);
}
}
#[test]
fn from_str_delegates_to_parse_period() {
assert_eq!(
"6M".parse::<Period>().unwrap(),
Period::new(6, TimeUnit::Months)
);
assert!("6X".parse::<Period>().is_err());
}
}