227 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			227 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
use std::{
 | 
						|
    fmt::{Display, Formatter, Result as FmtResult},
 | 
						|
    str::from_utf8,
 | 
						|
    time::{SystemTime, UNIX_EPOCH},
 | 
						|
};
 | 
						|
 | 
						|
use chrono::{DateTime, Datelike, Timelike, Utc};
 | 
						|
use chrono_tz::Tz;
 | 
						|
use cron_parser::parse;
 | 
						|
use tokio::process::Command;
 | 
						|
 | 
						|
use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION};
 | 
						|
 | 
						|
#[derive(Debug)]
 | 
						|
pub enum InvalidTime {
 | 
						|
    ParseErrorDMY,
 | 
						|
    ParseErrorHMS,
 | 
						|
    ParseErrorDisplacement,
 | 
						|
    ParseErrorChrono,
 | 
						|
}
 | 
						|
 | 
						|
impl Display for InvalidTime {
 | 
						|
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
 | 
						|
        write!(f, "InvalidTime: {:?}", self)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
impl std::error::Error for InvalidTime {}
 | 
						|
 | 
						|
#[derive(Copy, Clone)]
 | 
						|
enum ParseType {
 | 
						|
    Explicit,
 | 
						|
    Displacement,
 | 
						|
}
 | 
						|
 | 
						|
#[derive(Clone)]
 | 
						|
pub struct TimeParser {
 | 
						|
    timezone: Tz,
 | 
						|
    inverted: bool,
 | 
						|
    time_string: String,
 | 
						|
    parse_type: ParseType,
 | 
						|
}
 | 
						|
 | 
						|
impl TryFrom<&TimeParser> for i64 {
 | 
						|
    type Error = InvalidTime;
 | 
						|
 | 
						|
    fn try_from(value: &TimeParser) -> Result<Self, Self::Error> {
 | 
						|
        value.timestamp()
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
impl TimeParser {
 | 
						|
    pub fn new(input: &str, timezone: Tz) -> Self {
 | 
						|
        let inverted = input.starts_with('-');
 | 
						|
 | 
						|
        let parse_type = if input.contains('/') || input.contains(':') {
 | 
						|
            ParseType::Explicit
 | 
						|
        } else {
 | 
						|
            ParseType::Displacement
 | 
						|
        };
 | 
						|
 | 
						|
        Self {
 | 
						|
            timezone,
 | 
						|
            inverted,
 | 
						|
            time_string: input.trim_start_matches('-').to_string(),
 | 
						|
            parse_type,
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    pub fn timestamp(&self) -> Result<i64, InvalidTime> {
 | 
						|
        match self.parse_type {
 | 
						|
            ParseType::Explicit => Ok(self.process_explicit()?),
 | 
						|
 | 
						|
            ParseType::Displacement => {
 | 
						|
                let now = SystemTime::now();
 | 
						|
                let since_epoch = now
 | 
						|
                    .duration_since(UNIX_EPOCH)
 | 
						|
                    .expect("Time calculated as going backwards. Very bad");
 | 
						|
 | 
						|
                Ok(since_epoch.as_secs() as i64 + self.process_displacement()?)
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    pub fn displacement(&self) -> Result<i64, InvalidTime> {
 | 
						|
        match self.parse_type {
 | 
						|
            ParseType::Explicit => {
 | 
						|
                let now = SystemTime::now();
 | 
						|
                let since_epoch = now
 | 
						|
                    .duration_since(UNIX_EPOCH)
 | 
						|
                    .expect("Time calculated as going backwards. Very bad");
 | 
						|
 | 
						|
                Ok(self.process_explicit()? - since_epoch.as_secs() as i64)
 | 
						|
            }
 | 
						|
 | 
						|
            ParseType::Displacement => Ok(self.process_displacement()?),
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    fn process_explicit(&self) -> Result<i64, InvalidTime> {
 | 
						|
        let mut time = Utc::now().with_timezone(&self.timezone).with_second(0).unwrap();
 | 
						|
 | 
						|
        let mut segments = self.time_string.rsplit('-');
 | 
						|
        // this segment will always exist even if split fails
 | 
						|
        let hms = segments.next().unwrap();
 | 
						|
 | 
						|
        let h_m_s = hms.split(':');
 | 
						|
 | 
						|
        for (t, setter) in
 | 
						|
            h_m_s.take(3).zip(&[DateTime::with_hour, DateTime::with_minute, DateTime::with_second])
 | 
						|
        {
 | 
						|
            time = setter(&time, t.parse().map_err(|_| InvalidTime::ParseErrorHMS)?)
 | 
						|
                .map_or_else(|| Err(InvalidTime::ParseErrorHMS), Ok)?;
 | 
						|
        }
 | 
						|
 | 
						|
        if let Some(dmy) = segments.next() {
 | 
						|
            let mut d_m_y = dmy.split('/');
 | 
						|
 | 
						|
            let day = d_m_y.next();
 | 
						|
            let month = d_m_y.next();
 | 
						|
            let year = d_m_y.next();
 | 
						|
 | 
						|
            for (t, setter) in [day, month].iter().zip(&[DateTime::with_day, DateTime::with_month])
 | 
						|
            {
 | 
						|
                if let Some(t) = t {
 | 
						|
                    time = setter(&time, t.parse().map_err(|_| InvalidTime::ParseErrorDMY)?)
 | 
						|
                        .map_or_else(|| Err(InvalidTime::ParseErrorDMY), Ok)?;
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            if let Some(year) = year {
 | 
						|
                if year.len() == 4 {
 | 
						|
                    time = time
 | 
						|
                        .with_year(year.parse().map_err(|_| InvalidTime::ParseErrorDMY)?)
 | 
						|
                        .map_or_else(|| Err(InvalidTime::ParseErrorDMY), Ok)?;
 | 
						|
                } else if year.len() == 2 {
 | 
						|
                    time = time
 | 
						|
                        .with_year(
 | 
						|
                            format!("20{}", year)
 | 
						|
                                .parse()
 | 
						|
                                .map_err(|_| InvalidTime::ParseErrorDMY)?,
 | 
						|
                        )
 | 
						|
                        .map_or_else(|| Err(InvalidTime::ParseErrorDMY), Ok)?;
 | 
						|
                } else {
 | 
						|
                    return Err(InvalidTime::ParseErrorDMY);
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        Ok(time.timestamp() as i64)
 | 
						|
    }
 | 
						|
 | 
						|
    fn process_displacement(&self) -> Result<i64, InvalidTime> {
 | 
						|
        let mut current_buffer = "0".to_string();
 | 
						|
 | 
						|
        let mut seconds = 0_i64;
 | 
						|
        let mut minutes = 0_i64;
 | 
						|
        let mut hours = 0_i64;
 | 
						|
        let mut days = 0_i64;
 | 
						|
 | 
						|
        for character in self.time_string.chars() {
 | 
						|
            match character {
 | 
						|
                's' => {
 | 
						|
                    seconds = current_buffer.parse::<i64>().unwrap();
 | 
						|
                    current_buffer = String::from("0");
 | 
						|
                }
 | 
						|
 | 
						|
                'm' => {
 | 
						|
                    minutes = current_buffer.parse::<i64>().unwrap();
 | 
						|
                    current_buffer = String::from("0");
 | 
						|
                }
 | 
						|
 | 
						|
                'h' => {
 | 
						|
                    hours = current_buffer.parse::<i64>().unwrap();
 | 
						|
                    current_buffer = String::from("0");
 | 
						|
                }
 | 
						|
 | 
						|
                'd' => {
 | 
						|
                    days = current_buffer.parse::<i64>().unwrap();
 | 
						|
                    current_buffer = String::from("0");
 | 
						|
                }
 | 
						|
 | 
						|
                c => {
 | 
						|
                    if c.is_digit(10) {
 | 
						|
                        current_buffer += &c.to_string();
 | 
						|
                    } else {
 | 
						|
                        return Err(InvalidTime::ParseErrorDisplacement);
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        let full = (seconds
 | 
						|
            + (minutes * 60)
 | 
						|
            + (hours * 3600)
 | 
						|
            + (days * 86400)
 | 
						|
            + current_buffer.parse::<i64>().unwrap())
 | 
						|
            * if self.inverted { -1 } else { 1 };
 | 
						|
 | 
						|
        Ok(full)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
pub async fn natural_parser(time: &str, timezone: &str) -> Option<i64> {
 | 
						|
    Command::new(&*PYTHON_LOCATION)
 | 
						|
        .arg("-c")
 | 
						|
        .arg(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/dp.py")))
 | 
						|
        .arg(time)
 | 
						|
        .arg(timezone)
 | 
						|
        .arg(&*LOCAL_TIMEZONE)
 | 
						|
        .output()
 | 
						|
        .await
 | 
						|
        .ok()
 | 
						|
        .and_then(|inner| {
 | 
						|
            if inner.status.success() {
 | 
						|
                Some(from_utf8(&*inner.stdout).unwrap().parse::<i64>().unwrap())
 | 
						|
            } else {
 | 
						|
                None
 | 
						|
            }
 | 
						|
        })
 | 
						|
        .and_then(|inner| if inner < 0 { None } else { Some(inner) })
 | 
						|
}
 | 
						|
 | 
						|
pub fn cron_next_timestamp(expr: &str, timezone: Tz) -> Option<i64> {
 | 
						|
    parse(expr, &Utc::now().with_timezone(&timezone)).ok().map(|next| next.timestamp() as i64)
 | 
						|
}
 |