///|
/// tz_cron.mbt — cron 表达式(5 字段:分 时 日 月 周)
///|
/// 月份名称(小写,jan=1..dec=12)
let cron_month_names : Array[String] = [
"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec",
]
///|
/// 星期名称(小写,sun=0..sat=6)
let cron_dow_names : Array[String] = [
"sun", "mon", "tue", "wed", "thu", "fri", "sat",
]
///|
pub struct Cron {
minutes : Array[Bool] // 60 (0-59)
hours : Array[Bool] // 24 (0-23)
doms : Array[Bool] // 32 (1-31)
months : Array[Bool] // 13 (1-12)
dows : Array[Bool] // 7 (0-6)
dom_restricted : Bool // dom 是否非 *
dow_restricted : Bool // dow 是否非 *
} derive(Debug)
///|
pub extend Cron with Debug::{to_repr}
///|
/// 转小写
fn to_lower(s : String) -> String {
let mut r = ""
for c in s {
if c >= 'A' && c <= 'Z' {
match (c.to_int() + 32).to_char() {
Some(ch) => r += ch.to_string()
None => r += c.to_string()
}
} else {
r += c.to_string()
}
}
r
}
///|
/// 解析整数
fn parse_cron_int(s : String) -> Int32? {
if s.length() == 0 {
return None
}
let chars = s.to_array()
let mut result = 0
let mut i = 0
while i < chars.length() {
if chars[i] < '0' || chars[i] > '9' {
return None
}
result = result * 10 + (chars[i].to_int() - 48)
i += 1
}
Some(result)
}
///|
/// 按字符分割字符串
fn split_char(s : String, sep : Char) -> Array[String] {
let result : Array[String] = []
let mut word = ""
for c in s {
if c == sep {
result.push(word)
word = ""
} else {
word += c.to_string()
}
}
result.push(word)
result
}
///|
/// 按空白分割字符串
fn split_ws(s : String) -> Array[String] {
let result : Array[String] = []
let chars = s.to_array()
let n = chars.length()
let mut i = 0
while i < n {
while i < n && (chars[i] == ' ' || chars[i] == '\t') {
i += 1
}
if i >= n {
break
}
let mut word = ""
while i < n && chars[i] != ' ' && chars[i] != '\t' {
word += chars[i].to_string()
i += 1
}
result.push(word)
}
result
}
///|
/// 解析 cron 单值(数字或名称)
fn parse_cron_value(
s : String,
min : Int32,
max : Int32,
names : Array[String]?,
) -> Int32? {
match parse_cron_int(s) {
Some(n) => if n >= min && n <= max { Some(n) } else { None }
None =>
match names {
Some(tbl) => {
let lower = to_lower(s)
for i in 0.. None
}
}
}
///|
/// 解析 cron 字段,返回位集数组
fn parse_cron_field(
s : String,
min : Int32,
max : Int32,
names : Array[String]?,
) -> Array[Bool]? {
let bits : Array[Bool] = []
for _ in 0..<(max + 1) {
bits.push(false)
}
for part in split_char(s, ',') {
let step_parts = split_char(part, '/')
let base = step_parts[0]
let step = if step_parts.length() > 1 {
match parse_cron_int(step_parts[1]) {
Some(n) => n
None => return None
}
} else {
1
}
if step <= 0 {
return None
}
let lo_hi : (Int32, Int32)? = if base == "*" {
Some((min, max))
} else {
let range_parts = split_char(base, '-')
if range_parts.length() == 2 {
match
(
parse_cron_value(range_parts[0], min, max, names),
parse_cron_value(range_parts[1], min, max, names),
) {
(Some(l), Some(h)) => Some((l, h))
_ => None
}
} else {
match parse_cron_value(base, min, max, names) {
Some(v) => Some((v, v))
None => None
}
}
}
guard lo_hi is Some((lo, hi)) else { return None }
if lo < min || hi > max || lo > hi {
return None
}
let mut i = lo
while i <= hi {
bits[i] = true
i += step
}
}
Some(bits)
}
///|
/// 检查位集是否全 true
fn all_true(bits : Array[Bool], min : Int32, max : Int32) -> Bool {
for i in min..<(max + 1) {
if !bits[i] {
return false
}
}
true
}
///|
/// 解析 cron 表达式 "30 9 * * mon-fri" -> Cron?
/// 5 字段:minute(0-59) hour(0-23) day-of-month(1-31) month(1-12) day-of-week(0-6)
/// 支持:* n n-m n-m/s */s a,b,c 及名称(sun-sat, jan-dec)
pub fn Cron::parse(expr : String) -> Cron? {
let fields = split_ws(expr)
if fields.length() != 5 {
return None
}
guard parse_cron_field(fields[0], 0, 59, None) is Some(minutes) else {
return None
}
guard parse_cron_field(fields[1], 0, 23, None) is Some(hours) else {
return None
}
guard parse_cron_field(fields[2], 1, 31, None) is Some(doms) else {
return None
}
guard parse_cron_field(fields[3], 1, 12, Some(cron_month_names))
is Some(months) else {
return None
}
guard parse_cron_field(fields[4], 0, 6, Some(cron_dow_names)) is Some(dows) else {
return None
}
let dom_restricted = !all_true(doms, 1, 31)
let dow_restricted = !all_true(dows, 0, 6)
Some(Cron::{
minutes,
hours,
doms,
months,
dows,
dom_restricted,
dow_restricted,
})
}
///|
/// 检查 DateTime 是否匹配 cron 表达式。
/// 标准 cron 语义:dom 和 dow 都指定时取 OR,否则取 AND。
pub fn Cron::matches(self : Cron, dt : DateTime) -> Bool {
let ldt = dt.naive_local()
let min_ok = self.minutes[ldt.minute().to_int()]
let hour_ok = self.hours[ldt.hour().to_int()]
let month_ok = self.months[ldt.month().to_int()]
let dom_ok = self.doms[ldt.day().to_int()]
let dow_ok = self.dows[ldt.weekday().to_int()]
let day_ok = if self.dom_restricted && self.dow_restricted {
dom_ok || dow_ok
} else {
dom_ok && dow_ok
}
min_ok && hour_ok && month_ok && day_ok
}
///|
/// 从 dt 之后找下一次匹配时间(逐分钟搜索,最多 366 天)。
/// 返回 DateTime?(未找到返回 None)。
pub fn Cron::next(self : Cron, dt : DateTime) -> DateTime? {
let tz_id = dt.offset().tz_id()
guard Tz::from_str(tz_id) is Some(tz) else { return None }
let span = tz.timespans()
let start_ts = dt.timestamp() + 60L
let max_ts = start_ts + 366L * 24L * 60L * 60L
let mut ts = start_ts
while ts <= max_ts {
let i = binary_search(0, span.len(), fn(n) { span.utc_span(n).cmp(ts) })
let idx = match i {
Ok(n) => n
Err(n) => n
}
let off = TzOffset::make(tz, span.get(idx))
let candidate = DateTime::make(ts, off)
if self.matches(candidate) {
return Some(candidate)
}
ts += 60L
}
None
}