use super::{AfFormat, Keystroke, KeystrokeResult, Thrown, within_bounds_or_zero};
use crate::error::{AlertMessage, Error};
use crate::time::{parse_date_as_gmt, parse_date_with_fallback, print_date_using_format};
const FORMAT_CALLER: &str = "AFDate_FormatEx";
const KEYSTROKE_CALLER: &str = "AFDate_KeystrokeEx";
pub const DATE_FORMATS: [&str; 14] = [
"m/d",
"m/d/yy",
"mm/dd/yy",
"mm/yy",
"d-mmm",
"d-mmm-yy",
"dd-mmm-yy",
"yy-mm-dd",
"mmm-yy",
"mmmm-yy",
"mmm d, yyyy",
"mmmm d, yyyy",
"m/d/yy h:MM tt",
"m/d/yy HH:MM",
];
pub const TIME_FORMATS: [&str; 4] = ["HH:MM", "h:MM tt", "HH:MM:ss", "h:MM:ss tt"];
pub fn af_date_format_ex(value: &str, format: &str, now_ms: f64) -> Result<AfFormat, Thrown> {
if value.is_empty() {
return Ok(AfFormat::unchanged());
}
let d_date = if value.contains("GMT") {
parse_date_as_gmt(value)
} else {
parse_date_with_fallback(value, format, now_ms).0
};
if d_date.is_nan() {
return Err(Thrown::alerting(
Error::ParseDate {
format: format.to_string(),
},
FORMAT_CALLER,
AlertMessage::ParseDate {
format: format.to_string(),
},
));
}
Ok(AfFormat::formatted(print_date_using_format(d_date, format)))
}
#[must_use]
pub fn af_date_keystroke_ex(event: &Keystroke, format: &str, now_ms: f64) -> KeystrokeResult {
if !event.will_commit || event.value.is_empty() {
return KeystrokeResult::accept();
}
let (parsed, wrong_format) = parse_date_with_fallback(&event.value, format, now_ms);
if wrong_format || parsed.is_nan() {
return KeystrokeResult::reject_with(
KEYSTROKE_CALLER,
AlertMessage::ParseDate {
format: format.to_string(),
},
);
}
KeystrokeResult::accept()
}
fn picture(table: &[&'static str], index: i32) -> &'static str {
let i = within_bounds_or_zero(index, table.len());
table
.get(i)
.or_else(|| table.first())
.copied()
.unwrap_or("")
}
pub fn af_date_format(value: &str, index: i32, now_ms: f64) -> Result<AfFormat, Thrown> {
af_date_format_ex(value, picture(&DATE_FORMATS, index), now_ms)
}
#[must_use]
pub fn af_date_keystroke(event: &Keystroke, index: i32, now_ms: f64) -> KeystrokeResult {
af_date_keystroke_ex(event, picture(&DATE_FORMATS, index), now_ms)
}
pub fn af_time_format(value: &str, index: i32, now_ms: f64) -> Result<AfFormat, Thrown> {
af_date_format_ex(value, picture(&TIME_FORMATS, index), now_ms)
}
#[must_use]
pub fn af_time_keystroke(event: &Keystroke, index: i32, now_ms: f64) -> KeystrokeResult {
af_date_keystroke_ex(event, picture(&TIME_FORMATS, index), now_ms)
}
pub fn af_time_format_ex(value: &str, format: &str, now_ms: f64) -> Result<AfFormat, Thrown> {
af_date_format_ex(value, format, now_ms)
}
#[must_use]
pub fn af_time_keystroke_ex(event: &Keystroke, format: &str, now_ms: f64) -> KeystrokeResult {
af_date_keystroke_ex(event, format, now_ms)
}
pub fn af_parse_date_ex(value: &str, format: &str, now_ms: f64) -> Result<f64, Thrown> {
let parsed = parse_date_with_fallback(value, format, now_ms).0;
if parsed.is_nan() {
return Err(Thrown::alerting(
Error::ParseDate {
format: format.to_string(),
},
"AFParseDateEx",
AlertMessage::ParseDate {
format: format.to_string(),
},
));
}
Ok(parsed)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::af::AfOutcome;
use crate::time::{ms_from_civil, year_from_time};
const NOW: f64 = 1_399_672_130_000.0;
fn formatted(value: &str, format: &str) -> String {
match af_date_format_ex(value, format, NOW)
.expect("parsed")
.outcome
{
AfOutcome::Formatted(s) => s,
AfOutcome::Unchanged => value.to_string(),
AfOutcome::Rejected => unreachable!("AFDate_FormatEx never rejects"),
}
}
fn by_index(value: &str, index: i32) -> String {
match af_date_format(value, index, NOW).expect("parsed").outcome {
AfOutcome::Formatted(s) => s,
other => unreachable!("AFDate_Format answered {other:?}"),
}
}
#[test]
fn date_format_transcript_lines() {
assert_eq!(by_index("GMT", 1), "1/1/70");
assert_eq!(by_index("PDT", 1), "5/9/14");
assert_eq!(by_index("GMT", 0), "1/1");
assert_eq!(by_index("PDT", 0), "5/9");
assert_eq!(formatted("x", "2"), "2");
assert_eq!(formatted("x", "blooey"), "blooey");
assert_eq!(formatted("x", "m/d"), "5/9");
assert_eq!(formatted("12302015", "mm/dd/yyyy"), "12/02/2015");
assert_eq!(formatted("20122015", "mm/dd/yyyy"), "05/09/2014");
}
#[test]
fn a_picture_that_matches_nothing_still_prints() {
assert_eq!(formatted("0", "blooey"), "blooey");
assert_eq!(af_time_format("0", 1, NOW).unwrap().outcome, {
AfOutcome::Formatted("9:48 pm".to_string())
});
}
#[test]
fn concatenated_digits_are_misread_and_that_is_the_behaviour() {
assert_eq!(formatted("12302015", "mm/dd/yyyy"), "12/02/2015");
assert_eq!(formatted("20122015", "mm/dd/yyyy"), "05/09/2014");
}
#[test]
fn the_gmt_path_needs_exactly_eight_tokens() {
assert_eq!(formatted("GMT", "m/d/yy"), "1/1/70");
assert_eq!(
formatted("Tue Aug 11 14:24:16 GMT+08002009", "m/d/yy"),
"1/1/70"
);
assert_eq!(
formatted("Tue Aug 11 14:24:16 GMT 2009", "m/d/yyyy"),
"8/11/2009"
);
}
#[test]
fn an_empty_value_is_left_alone() {
let out = af_date_format_ex("", "mm/dd/yyyy", NOW).unwrap();
assert_eq!(out.outcome, AfOutcome::Unchanged);
assert!(out.effects.is_empty());
}
#[test]
fn two_digit_years_pivot_at_fifty() {
for (typed, expected) in [
("311285", 1985),
("010250", 1950),
("010249", 2049),
("010205", 2005),
("010200", 2000),
] {
let t = af_parse_date_ex(typed, "ddmmyy", 0.0).unwrap();
assert_eq!(year_from_time(t), expected, "{typed}");
}
assert_eq!(
year_from_time(af_parse_date_ex("01021985", "ddmmyyyy", 0.0).unwrap()),
1985
);
}
#[test]
fn keystroke_rejects_only_on_commit_and_never_throws() {
let out = af_date_keystroke_ex(&Keystroke::insert("x", "y", 1), "m/d", NOW);
assert!(!out.is_reject());
for (value, format, expected) in [
(
"x",
"2",
"AFDate_KeystrokeEx[icon=3,type=0]: The input value can't be parsed as a valid date/time (2).",
),
(
"x",
"blooey",
"AFDate_KeystrokeEx[icon=3,type=0]: The input value can't be parsed as a valid date/time (blooey).",
),
(
"x",
"m/d",
"AFDate_KeystrokeEx[icon=3,type=0]: The input value can't be parsed as a valid date/time (m/d).",
),
] {
let out = af_date_keystroke_ex(&Keystroke::commit(value), format, NOW);
assert!(out.is_reject(), "{value:?}/{format:?}");
assert_eq!(
out.alert().map(ToString::to_string).as_deref(),
Some(expected)
);
}
}
#[test]
fn well_formed_dates_commit_quietly() {
for (value, index) in [("04/19", 2), ("04/19/15", 0)] {
let out = af_date_keystroke(&Keystroke::commit(value), index, NOW);
assert!(!out.is_reject(), "{value:?}");
assert!(out.effects.is_empty());
}
assert!(!af_time_keystroke(&Keystroke::commit("12:03"), 65, NOW).is_reject());
assert!(!af_time_keystroke_ex(&Keystroke::commit("12:04"), "blooey", NOW).is_reject());
}
#[test]
fn the_time_entry_points_reuse_the_date_machinery() {
assert_eq!(DATE_FORMATS.len(), 14);
assert_eq!(TIME_FORMATS.len(), 4);
for index in [-1, 4, 99] {
assert_eq!(
af_time_format("0", index, NOW).unwrap().outcome,
af_time_format_ex("0", TIME_FORMATS[0], NOW)
.unwrap()
.outcome,
"index {index}"
);
}
}
#[test]
fn a_hopeless_parse_throws_with_its_notification() {
let out = af_parse_date_ex("1", "2", NOW).unwrap();
assert!((out - NOW).abs() < 1.0, "{out}");
}
#[test]
fn printing_round_trips_a_civil_date() {
let t = ms_from_civil(1968, 6, 25, 0, 0, 0);
assert_eq!(
af_date_format_ex(
&crate::time::print_date_using_format(t, "dd/mm/yyyy"),
"ddmmyy",
NOW
)
.unwrap()
.outcome,
AfOutcome::Formatted("250668".to_string())
);
}
}