use crate::digits::{digit_run, digits_value};
use crate::words::{at, chars, is_digit, is_whitespace, string, sub, tail};
use super::emphasis::{emphasis_run, without_markers};
use super::types::{Parts, TimeValue};
const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TimeStyle {
pub hours: bool,
pub hour_width: usize,
pub minute_width: usize,
pub long_minutes: bool,
pub frac_sep: Option<char>,
pub frac_digits: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct TimeScan {
pub parts: Parts,
pub time: Option<TimeValue>,
pub style: Option<TimeStyle>,
pub invalid: bool,
pub text: String,
}
fn is_gap_separator(ch: Option<char>) -> bool {
matches!(ch, Some('-' | '–' | '—' | '|' | '.' | ':'))
}
fn skip_whitespace(s: &[char], from: usize) -> usize {
let mut i = from;
while is_whitespace(at(s, i)) {
i += 1;
}
i
}
fn is_bullet(ch: Option<char>) -> bool {
matches!(ch, Some('-' | '–' | '—' | '*' | '•'))
}
fn untimed(raw: &[char], invalid: bool) -> TimeScan {
let start = skip_whitespace(raw, 0);
let mut lead = 0;
if is_bullet(at(raw, start)) && is_whitespace(at(raw, start + 1)) {
lead = skip_whitespace(raw, start + 1);
}
TimeScan {
parts: Parts {
lead: string(sub(raw, 0, lead)),
time: String::new(),
gap: String::new(),
rest: string(tail(raw, lead)),
},
time: None,
style: None,
invalid,
text: without_markers(raw, lead, None),
}
}
fn closing_run(raw: &[char], from: usize, open: Option<char>) -> usize {
if open.is_some() && at(raw, from) == open {
from + emphasis_run(raw, from)
} else {
from
}
}
pub(crate) fn seconds_of(whole: u32, frac: &str) -> f64 {
if frac.is_empty() {
f64::from(whole)
} else {
format!("{whole}.{frac}").parse().unwrap_or(f64::from(whole))
}
}
struct Shape<'a> {
parts: Vec<&'a [char]>,
end: usize,
}
fn shape_at(s: &[char], from: usize) -> Option<Shape<'_>> {
let na = digit_run(s, from);
if na == 0 || at(s, from + na) != Some(':') || !is_digit(at(s, from + na + 1)) {
return None;
}
let mut parts = vec![sub(s, from, from + na)];
let mut end = from + na;
while at(s, end) == Some(':') && is_digit(at(s, end + 1)) {
let n = digit_run(s, end + 1);
parts.push(sub(s, end + 1, end + 1 + n));
end += 1 + n;
}
Some(Shape { parts, end })
}
pub(crate) fn shape_seconds(parts: &[&[char]]) -> Option<u32> {
match parts {
[a, b, c] => {
if a.is_empty() || a.len() > 2 || b.len() != 2 || c.len() != 2 {
return None;
}
let (m, sec) = (digits_value(b), digits_value(c));
if m > 59 || sec > 59 {
return None;
}
Some(digits_value(a) * 3600 + m * 60 + sec)
}
[a, b] => {
if a.is_empty() || a.len() > 3 || b.len() != 2 {
return None;
}
let sec = digits_value(b);
if sec > 59 {
return None;
}
Some(digits_value(a) * 60 + sec)
}
_ => None,
}
}
fn starts_with(s: &[char], from: usize, word: &str) -> bool {
let n = word.chars().count();
sub(s, from, from + n).iter().copied().eq(word.chars())
}
pub(crate) fn scan_time(raw: &str) -> TimeScan {
let raw = &chars(raw);
let mut i = skip_whitespace(raw, 0);
if is_bullet(at(raw, i)) && is_whitespace(at(raw, i + 1)) {
i = skip_whitespace(raw, i + 1);
}
let mut outer_open: Option<char> = None;
let mut n = emphasis_run(raw, i);
if n > 0 && (matches!(at(raw, i + n), Some('[' | '(')) || is_digit(at(raw, i + n))) {
outer_open = at(raw, i);
i += n;
}
let closer = match at(raw, i) {
Some('[') => Some(']'),
Some('(') => Some(')'),
_ => None,
};
let mut inner_open: Option<char> = None;
if closer.is_some() {
i += 1;
n = emphasis_run(raw, i);
if n > 0 && is_digit(at(raw, i + n)) {
inner_open = at(raw, i);
i += n;
}
}
let lead_end = i;
let Some(shape) = shape_at(raw, i) else { return untimed(raw, false) };
let Some(whole) = shape_seconds(&shape.parts) else { return untimed(raw, true) }; let mut j = shape.end;
let mut frac_sep: Option<char> = None;
let mut frac = String::new();
if let Some(sep @ ('.' | ',')) = at(raw, j) {
let n = digit_run(raw, j + 1);
if (1..=3).contains(&n) {
frac_sep = Some(sep);
frac = string(sub(raw, j + 1, j + 1 + n));
j += 1 + n;
}
}
let time_end = j;
let follows_time =
|k: usize| k >= raw.len() || is_whitespace(at(raw, k)) || is_gap_separator(at(raw, k));
let mut open: Option<char> = None;
if let Some(closer) = closer {
let mut k = closing_run(raw, j, inner_open);
if k == j {
open = inner_open;
}
j = k;
if at(raw, j) != Some(closer) {
return untimed(raw, false);
}
j += 1;
k = closing_run(raw, j, outer_open);
if k > j && follows_time(k) {
j = k;
} else {
open = outer_open.or(open);
}
} else {
let k = closing_run(raw, j, outer_open);
if k == j {
open = outer_open;
}
j = k;
if !follows_time(j) {
return untimed(raw, false);
}
}
j = skip_whitespace(raw, j);
if is_gap_separator(at(raw, j)) {
j = skip_whitespace(raw, j + 1);
}
if starts_with(raw, j, "==>") {
j = skip_whitespace(raw, j + 3);
} else if starts_with(raw, j, "=>") {
j = skip_whitespace(raw, j + 2);
}
let time_raw = string(sub(raw, lead_end, time_end));
let hours = shape.parts.len() == 3;
let first = shape.parts.first().copied().unwrap_or(&[]);
let second = shape.parts.get(1).copied().unwrap_or(&[]);
let long_minutes = !hours && digits_value(first) >= 60;
TimeScan {
parts: Parts {
lead: string(sub(raw, 0, lead_end)),
time: time_raw.clone(),
gap: string(sub(raw, time_end, j)),
rest: string(tail(raw, j)),
},
time: Some(TimeValue { raw: time_raw, seconds: seconds_of(whole, &frac) }),
style: Some(TimeStyle {
hours,
hour_width: if hours { first.len() } else { 0 },
minute_width: if hours {
second.len()
} else if long_minutes {
first.len().min(2)
} else {
first.len()
},
long_minutes,
frac_sep,
frac_digits: frac.chars().count(),
}),
invalid: false,
text: without_markers(raw, j, open),
}
}
fn pad(n: u64, width: usize) -> String {
format!("{n:0width$}")
}
pub(crate) struct FormattedTime {
pub raw: String,
pub upgraded: bool,
}
pub(crate) fn format_time(
seconds: f64,
style: &TimeStyle,
one_digit_hours: bool,
) -> Option<FormattedTime> {
let scale: f64 = match style.frac_digits {
0 => 1.0,
1 => 10.0,
2 => 100.0,
_ => 1000.0,
};
let units = (seconds * scale + 0.5).floor();
if !units.is_finite() || units.abs() > MAX_SAFE_INTEGER || units < 0.0 {
return None;
}
let whole_f = (units / scale).floor();
let frac_units = (units - whole_f * scale) as u64;
if whole_f >= 100.0 * 3600.0 {
return None;
}
let whole = whole_f as u64;
let frac = match style.frac_sep {
Some(sep) if style.frac_digits > 0 => {
format!("{sep}{}", pad(frac_units, style.frac_digits))
}
_ => String::new(),
};
let sec = whole % 60;
if style.hours {
let h = whole / 3600;
let m = (whole % 3600) / 60;
return Some(FormattedTime {
raw: format!(
"{}:{}:{}{frac}",
pad(h, style.hour_width),
pad(m, style.minute_width),
pad(sec, 2)
),
upgraded: false,
});
}
if style.long_minutes {
let m = whole / 60;
if m > 999 {
return None; }
return Some(FormattedTime {
raw: format!("{}:{}{frac}", pad(m, style.minute_width), pad(sec, 2)),
upgraded: false,
});
}
if whole >= 3600 {
let h = whole / 3600;
let m = (whole % 3600) / 60;
return Some(FormattedTime {
raw: format!(
"{}:{}:{}{frac}",
pad(h, if one_digit_hours { 1 } else { 2 }),
pad(m, 2),
pad(sec, 2)
),
upgraded: true,
});
}
let m = whole / 60;
Some(FormattedTime {
raw: format!("{}:{}{frac}", pad(m, style.minute_width), pad(sec, 2)),
upgraded: false,
})
}