use crate::convert;
use crate::error::{Error, Result};
use time::{
OffsetDateTime,
format_description::well_known::{Rfc2822, Rfc3339},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ResetTimeKind {
Seconds,
Timestamp,
TimestampMillis,
ImfFixdate,
Iso8601,
OpenAiDuration,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ResetTime {
Seconds(usize),
DateTime(OffsetDateTime),
}
impl ResetTime {
pub(crate) fn new(value: &str, kind: ResetTimeKind) -> Result<Self> {
match kind {
ResetTimeKind::Seconds => {
let s = convert::to_usize(value)?;
Ok(ResetTime::Seconds(s))
}
ResetTimeKind::Timestamp => {
let s = value.parse::<i64>().map_err(|_| Error::NoMatchingVariant)?;
let dt =
OffsetDateTime::from_unix_timestamp(s).map_err(|_| Error::NoMatchingVariant)?;
Ok(ResetTime::DateTime(dt))
}
ResetTimeKind::TimestampMillis => {
let ms = value
.parse::<i128>()
.map_err(|_| Error::NoMatchingVariant)?;
let dt = OffsetDateTime::from_unix_timestamp_nanos(ms * 1_000_000)
.map_err(|_| Error::NoMatchingVariant)?;
Ok(ResetTime::DateTime(dt))
}
ResetTimeKind::ImfFixdate => {
let dt =
OffsetDateTime::parse(value, &Rfc2822).map_err(|_| Error::NoMatchingVariant)?;
Ok(ResetTime::DateTime(dt))
}
ResetTimeKind::Iso8601 => {
let dt =
OffsetDateTime::parse(value, &Rfc3339).map_err(|_| Error::NoMatchingVariant)?;
Ok(ResetTime::DateTime(dt))
}
ResetTimeKind::OpenAiDuration => {
let seconds = parse_openai_duration(value).ok_or(Error::NoMatchingVariant)?;
Ok(ResetTime::Seconds(seconds))
}
}
}
#[must_use]
pub fn seconds(&self) -> usize {
match self {
ResetTime::Seconds(s) => *s,
#[allow(clippy::cast_possible_truncation)]
ResetTime::DateTime(d) => {
let diff = *d - OffsetDateTime::now_utc();
let seconds = diff.whole_seconds();
if seconds < 0 { 0 } else { seconds as usize }
}
}
}
#[must_use]
pub fn duration(&self) -> std::time::Duration {
match self {
ResetTime::Seconds(s) => std::time::Duration::from_secs(*s as u64),
ResetTime::DateTime(d) => {
let diff = *d - OffsetDateTime::now_utc();
std::time::Duration::try_from(diff).unwrap_or(std::time::Duration::ZERO)
}
}
}
}
impl TryFrom<&str> for ResetTime {
type Error = Error;
fn try_from(value: &str) -> Result<Self> {
if let Ok(n) = convert::to_usize(value) {
let kind = if n > 1_000_000_000 {
ResetTimeKind::Timestamp
} else {
ResetTimeKind::Seconds
};
return Self::new(value, kind);
}
if let Ok(r) = Self::new(value, ResetTimeKind::ImfFixdate) {
return Ok(r);
}
if let Ok(r) = Self::new(value, ResetTimeKind::Iso8601) {
return Ok(r);
}
Err(Error::NoMatchingVariant)
}
}
fn parse_openai_duration(s: &str) -> Option<usize> {
if s.is_empty() {
return None;
}
let mut total_ms = 0.0_f64;
let mut num_start: Option<usize> = None;
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
if c.is_ascii_digit() || c == b'.' {
if num_start.is_none() {
num_start = Some(i);
}
i += 1;
continue;
}
let start = num_start.take()?;
let val: f64 = s[start..i].parse().ok()?;
let (multiplier_ms, consumed) = match c {
b's' => (1_000.0, 1),
b'm' if bytes.get(i + 1) == Some(&b's') => (1.0, 2),
b'm' => (60_000.0, 1),
b'h' => (3_600_000.0, 1),
b'd' => (86_400_000.0, 1),
_ => return None,
};
total_ms += val * multiplier_ms;
i += consumed;
}
if num_start.is_some() {
return None;
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
Some((total_ms / 1000.0).ceil() as usize)
}
#[cfg(test)]
mod openai_duration_tests {
use super::parse_openai_duration;
#[test]
fn seconds() {
assert_eq!(parse_openai_duration("1s"), Some(1));
assert_eq!(parse_openai_duration("42s"), Some(42));
}
#[test]
fn milliseconds_round_up() {
assert_eq!(parse_openai_duration("500ms"), Some(1));
assert_eq!(parse_openai_duration("1000ms"), Some(1));
assert_eq!(parse_openai_duration("1001ms"), Some(2));
}
#[test]
fn minutes_vs_milliseconds() {
assert_eq!(parse_openai_duration("1m"), Some(60));
assert_eq!(parse_openai_duration("1ms"), Some(1));
}
#[test]
fn compound() {
assert_eq!(parse_openai_duration("1m30s"), Some(90));
assert_eq!(parse_openai_duration("1h2m3s"), Some(3723));
}
#[test]
fn fractional() {
assert_eq!(parse_openai_duration("1.5s"), Some(2));
assert_eq!(parse_openai_duration("0.5m"), Some(30));
}
#[test]
fn hours_and_days() {
assert_eq!(parse_openai_duration("1h"), Some(3600));
assert_eq!(parse_openai_duration("1d"), Some(86_400));
}
#[test]
fn invalid() {
assert_eq!(parse_openai_duration(""), None);
assert_eq!(parse_openai_duration("10"), None); assert_eq!(parse_openai_duration("s"), None); assert_eq!(parse_openai_duration("10x"), None); assert_eq!(parse_openai_duration("abc"), None);
}
}