use std::time::Duration;
use uuid::Uuid;
use crate::base::output::props::OutputProps;
use crate::base::{
block::{Block, BlockDesc, BlockProps, BlockState},
input::{InputProps, input_reader::InputReader},
output::Output,
};
use crate::blocks::utils::input_as_number_in;
use crate::tokio_impl::sleep::current_time_millis;
use libhaystack::units::units_generated::MINUTE;
use libhaystack::val::{Bool, Value, kind::HaystackKind};
use crate::{blocks::InputImpl, blocks::OutputImpl};
#[block]
#[derive(BlockProps, Debug)]
#[category = "time"]
pub struct Schedule {
#[input(kind = "Str")]
pub start: InputImpl,
#[input(kind = "Str")]
pub end: InputImpl,
#[input(kind = "Str")]
pub days: InputImpl,
#[input(name = "tzOffset", kind = "Number")]
pub tz_offset: InputImpl,
#[output(name = "occupied", kind = "Bool")]
pub out: OutputImpl,
}
impl Block for Schedule {
async fn execute(&mut self) {
self.wait_on_inputs(Duration::from_millis(1000)).await;
if !self.out.is_connected() {
return;
}
let start = match str_input(&self.start).and_then(parse_hhmm) {
Some(v) => v,
None => {
self.out.set(Bool { value: false }.into());
return;
}
};
let end = match str_input(&self.end).and_then(parse_hhmm) {
Some(v) => v,
None => {
self.out.set(Bool { value: false }.into());
return;
}
};
let days_mask = match str_input(&self.days).map(parse_days) {
Some(m) => m,
None => {
self.out.set(Bool { value: false }.into());
return;
}
};
let tz_offset_min = input_as_number_in(&self.tz_offset, &MINUTE)
.map(|v| v as i64)
.unwrap_or(0);
let utc_ms = current_time_millis() as i64;
let local_ms = utc_ms + tz_offset_min * 60_000;
let local_secs = local_ms.div_euclid(1000);
let days_since_epoch = local_secs.div_euclid(86_400);
let seconds_of_day = local_secs.rem_euclid(86_400) as u32;
let dow = ((days_since_epoch + 3).rem_euclid(7)) as u8;
let now_minutes = seconds_of_day / 60;
let occupied = if start == end {
false
} else if start < end {
(now_minutes >= start && now_minutes < end) && (days_mask & (1 << dow)) != 0
} else if now_minutes >= start {
(days_mask & (1 << dow)) != 0
} else if now_minutes < end {
let prev = (dow + 6) % 7;
(days_mask & (1 << prev)) != 0
} else {
false
};
self.out.set(Bool { value: occupied }.into());
}
}
fn str_input(input: &InputImpl) -> Option<&str> {
match &input.val {
Some(Value::Str(s)) => Some(s.value.as_str()),
_ => None,
}
}
fn parse_hhmm(s: &str) -> Option<u32> {
let (h, m) = s.split_once(':')?;
let h: u32 = h.trim().parse().ok()?;
let m: u32 = m.trim().parse().ok()?;
if h > 23 || m > 59 {
return None;
}
Some(h * 60 + m)
}
fn parse_days(s: &str) -> u8 {
let mut mask = 0u8;
for c in s.chars() {
let bit = match c.to_ascii_uppercase() {
'M' => 0,
'T' => 1,
'W' => 2,
'R' => 3,
'F' => 4,
'S' => 5,
'U' => 6,
_ => continue,
};
mask |= 1 << bit;
}
mask
}
#[cfg(test)]
mod test {
use super::{parse_days, parse_hhmm};
#[test]
fn test_parse_hhmm_ok() {
assert_eq!(parse_hhmm("00:00"), Some(0));
assert_eq!(parse_hhmm("08:30"), Some(8 * 60 + 30));
assert_eq!(parse_hhmm("23:59"), Some(23 * 60 + 59));
}
#[test]
fn test_parse_hhmm_invalid() {
assert_eq!(parse_hhmm("24:00"), None);
assert_eq!(parse_hhmm("12:60"), None);
assert_eq!(parse_hhmm("nope"), None);
assert_eq!(parse_hhmm(""), None);
}
#[test]
fn test_parse_days_weekdays() {
let m = parse_days("MTWRF");
assert_eq!(m, 0b0011111);
}
#[test]
fn test_parse_days_full_week() {
let m = parse_days("MTWRFSU");
assert_eq!(m, 0b1111111);
}
#[test]
fn test_parse_days_weekend() {
let m = parse_days("SU");
assert_eq!(m, 0b1100000);
}
#[test]
fn test_parse_days_ignores_unknown() {
assert_eq!(parse_days("M, T, W"), parse_days("MTW"));
}
}