///|
/// Human-readable description generator.
/// Converts a cron expression into a Chinese or English description.
///|
/// Generate an English description of a cron expression.
pub fn describe_en(fields : Array[CronField]) -> String {
if fields.length() != 5 && fields.length() != 6 {
return "Invalid cron expression"
}
let offset = if fields.length() == 6 { 1 } else { 0 }
let second_desc = if offset == 1 {
describe_field_en(fields[0], Second)
} else {
""
}
let minute_desc = describe_field_en(fields[offset], Minute)
let hour_desc = describe_field_en(fields[offset + 1], Hour)
let dom_desc = describe_field_en(fields[offset + 2], DayOfMonth)
let month_desc = describe_field_en(fields[offset + 3], Month)
let dow_desc = describe_field_en(fields[offset + 4], DayOfWeek)
build_description_en(
second_desc, minute_desc, hour_desc, dom_desc, month_desc, dow_desc, fields,
)
}
///|
/// Generate a Chinese description of a cron expression.
pub fn describe_cn(fields : Array[CronField]) -> String {
if fields.length() != 5 && fields.length() != 6 {
return "无效的 cron 表达式"
}
let offset = if fields.length() == 6 { 1 } else { 0 }
let second_desc = if offset == 1 {
describe_field_cn(fields[0], Second)
} else {
""
}
let minute_desc = describe_field_cn(fields[offset], Minute)
let hour_desc = describe_field_cn(fields[offset + 1], Hour)
let dom_desc = describe_field_cn(fields[offset + 2], DayOfMonth)
let month_desc = describe_field_cn(fields[offset + 3], Month)
let dow_desc = describe_field_cn(fields[offset + 4], DayOfWeek)
build_description_cn(
second_desc, minute_desc, hour_desc, dom_desc, month_desc, dow_desc, fields,
)
}
///|
/// Describe a single field in English.
fn describe_field_en(field : CronField, field_type : FieldType) -> String {
let raw = field.raw
let is_all = raw == "*"
let empty_names : Array[String] = []
match field_type {
Second => {
if is_all {
return "every second"
}
if raw.contains("/") && !raw.contains("-") {
match get_step(raw) {
Some(s) => return "every " + s.to_string() + " seconds"
None => ()
}
}
let vals = describe_values_en(field.values, empty_names)
if vals != "" {
"at second " + vals
} else {
""
}
}
Minute => {
if is_all {
return "every minute"
}
if raw.contains("/") && !raw.contains("-") {
match get_step(raw) {
Some(s) => return "every " + s.to_string() + " minutes"
None => ()
}
}
let vals = describe_values_en(field.values, empty_names)
if vals != "" {
"at minute " + vals
} else {
""
}
}
Hour => {
if is_all {
return ""
}
if raw.contains("/") && !raw.contains("-") {
match get_step(raw) {
Some(s) => return "every " + s.to_string() + " hours"
None => ()
}
}
let vals = describe_values_en(field.values, empty_names)
if vals != "" {
"at hour " + vals
} else {
""
}
}
DayOfMonth => {
if is_all {
return ""
}
let vals = describe_values_en(field.values, empty_names)
if vals != "" {
"on day " + vals + " of the month"
} else {
""
}
}
Month => {
if is_all {
return ""
}
let month_names = [
"", "January", "February", "March", "April", "May", "June", "July", "August",
"September", "October", "November", "December",
]
let vals = describe_values_en(field.values, month_names)
if vals != "" {
"in " + vals
} else {
""
}
}
DayOfWeek => {
if is_all {
return ""
}
let dow_names = [
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday",
"Sunday",
]
let vals = describe_values_en(field.values, dow_names)
if vals != "" {
"on " + vals
} else {
""
}
}
}
}
///|
/// Describe values as a human-readable string, optionally using name mapping.
fn describe_values_en(values : Array[Int], names : Array[String]) -> String {
if values.length() == 0 {
return ""
}
// Check if it's a simple range
if is_consecutive_range(values) {
let first = format_value(values[0], names)
let last = format_value(values[values.length() - 1], names)
return first + " through " + last
}
// Check if it's a step
if values.length() > 1 {
let step = values[1] - values[0]
let mut is_step = step > 1
let mut j = 1
while j < values.length() - 1 && is_step {
if values[j + 1] - values[j] != step {
is_step = false
}
j = j + 1
}
if is_step && step > 1 {
let first = format_value(values[0], names)
let last = format_value(values[values.length() - 1], names)
return "every " + step.to_string() + " from " + first + " to " + last
}
}
// List of individual values
let mut result = ""
for k = 0; k < values.length(); k = k + 1 {
if k > 0 {
if k == values.length() - 1 {
result = result + " and "
} else {
result = result + ", "
}
}
result = result + format_value(values[k], names)
}
result
}
///|
/// Format a value, using a name from the mapping if available.
fn format_value(value : Int, names : Array[String]) -> String {
if value >= 0 && value < names.length() && names[value] != "" {
names[value]
} else {
value.to_string()
}
}
///|
/// Check if values form a consecutive range.
fn is_consecutive_range(values : Array[Int]) -> Bool {
if values.length() <= 1 {
return false
}
let mut i = 1
while i < values.length() {
if values[i] != values[i - 1] + 1 {
return false
}
i = i + 1
}
true
}
///|
/// Extract step value from a "*/N" or "N/N" expression.
fn get_step(raw : String) -> Int? {
let pos = find_slash_pos(raw)
if pos >= 0 && pos + 1 < raw.length() {
return parse_int_desc(raw[pos + 1:raw.length()].to_owned())
}
None
}
///|
/// Build the final English description from field descriptions.
fn build_description_en(
second_desc : String,
minute_desc : String,
hour_desc : String,
dom_desc : String,
month_desc : String,
dow_desc : String,
fields : Array[CronField],
) -> String {
let parts : Array[String] = []
let offset = if fields.length() == 6 { 1 } else { 0 }
let minute_raw = fields[offset].raw
let hour_raw = fields[offset + 1].raw
// Time description
if minute_raw != "*" && hour_raw != "*" {
if minute_raw.contains("/") &&
!minute_raw.contains("-") &&
!minute_raw.contains(",") {
match get_step(minute_raw) {
Some(s) =>
if hour_raw.contains("-") &&
!hour_raw.contains(",") &&
!hour_raw.contains("/") {
let hour_vals = fields[offset + 1].values
if hour_vals.length() > 0 {
let desc = "every " +
s.to_string() +
" minutes between " +
hour_vals[0].to_string() +
":00 and " +
hour_vals[hour_vals.length() - 1].to_string() +
":59"
parts.push(desc)
} else {
parts.push("every " + s.to_string() + " minutes")
}
} else {
parts.push("every " + s.to_string() + " minutes")
}
None => {
if hour_desc != "" {
parts.push(hour_desc)
}
if minute_desc != "" {
parts.push(minute_desc)
}
}
}
} else {
if hour_desc != "" {
parts.push(hour_desc)
}
if minute_desc != "" {
parts.push(minute_desc)
}
}
} else if minute_raw != "*" {
if minute_desc != "" {
parts.push(minute_desc)
}
} else if hour_raw != "*" {
if hour_desc != "" {
parts.push(hour_desc)
}
} else if second_desc != "every second" {
parts.push("every minute")
}
if second_desc != "" {
parts.push(second_desc)
}
if dom_desc != "" {
parts.push(dom_desc)
}
if dow_desc != "" {
parts.push(dow_desc)
}
if month_desc != "" {
parts.push(month_desc)
}
if parts.length() == 0 {
return "every minute"
}
join_strings(parts, ", ")
}
///|
/// Build a Chinese description from field descriptions.
fn build_description_cn(
second_desc : String,
minute_desc : String,
hour_desc : String,
dom_desc : String,
month_desc : String,
dow_desc : String,
fields : Array[CronField],
) -> String {
let parts : Array[String] = []
let offset = if fields.length() == 6 { 1 } else { 0 }
let minute_raw = fields[offset].raw
let hour_raw = fields[offset + 1].raw
// Time description
if minute_raw != "*" && hour_raw != "*" {
if minute_raw.contains("/") &&
!minute_raw.contains("-") &&
!minute_raw.contains(",") {
match get_step(minute_raw) {
Some(s) =>
if hour_raw.contains("-") &&
!hour_raw.contains(",") &&
!hour_raw.contains("/") {
let hour_vals = fields[offset + 1].values
if hour_vals.length() > 0 {
parts.push(
"在" +
hour_vals[0].to_string() +
"点到" +
hour_vals[hour_vals.length() - 1].to_string() +
"点之间,每" +
s.to_string() +
"分钟",
)
} else {
parts.push("每" + s.to_string() + "分钟")
}
} else {
parts.push("每" + s.to_string() + "分钟")
}
None => {
if hour_desc != "" {
parts.push(hour_desc)
}
if minute_desc != "" {
parts.push(minute_desc)
}
}
}
} else {
if hour_desc != "" {
parts.push(hour_desc)
}
if minute_desc != "" {
parts.push(minute_desc)
}
}
} else if minute_raw != "*" {
if minute_desc != "" {
parts.push(minute_desc)
}
} else if hour_raw != "*" {
if hour_desc != "" {
parts.push(hour_desc)
}
} else if second_desc != "每秒" {
parts.push("每分钟")
}
if second_desc != "" {
parts.push(second_desc)
}
if dom_desc != "" {
parts.push(dom_desc)
}
if dow_desc != "" {
parts.push(dow_desc)
}
if month_desc != "" {
parts.push(month_desc)
}
if parts.length() == 0 {
return "每分钟"
}
join_strings(parts, ",")
}
///|
/// Describe a single field in Chinese.
fn describe_field_cn(field : CronField, field_type : FieldType) -> String {
let raw = field.raw
let is_all = raw == "*"
match field_type {
Second => {
if is_all {
return "每秒"
}
if raw.contains("/") && !raw.contains("-") {
match get_step(raw) {
Some(s) => return "每" + s.to_string() + "秒"
None => ()
}
}
let vals = describe_values_cn(field.values)
if vals != "" {
"第" + vals + "秒"
} else {
""
}
}
Minute => {
if is_all {
return "每分钟"
}
if raw.contains("/") && !raw.contains("-") {
match get_step(raw) {
Some(s) => return "每" + s.to_string() + "分钟"
None => ()
}
}
let vals = describe_values_cn(field.values)
if vals != "" {
"第" + vals + "分钟"
} else {
""
}
}
Hour => {
if is_all {
return ""
}
let vals = describe_values_cn(field.values)
if vals != "" {
vals + "点"
} else {
""
}
}
DayOfMonth => {
if is_all {
return ""
}
let vals = describe_values_cn(field.values)
if vals != "" {
"每月" + vals + "日"
} else {
""
}
}
Month => {
if is_all {
return ""
}
let vals = describe_values_cn(field.values)
if vals != "" {
vals + "月"
} else {
""
}
}
DayOfWeek => {
if is_all {
return ""
}
let dow_names = [
"周日", "周一", "周二", "周三", "周四", "周五", "周六", "周日",
]
let vals = describe_values_cn_names(field.values, dow_names)
if vals != "" {
"每" + vals
} else {
""
}
}
}
}
///|
/// Describe values in Chinese.
fn describe_values_cn(values : Array[Int]) -> String {
if values.length() == 0 {
return ""
}
if is_consecutive_range(values) {
if values.length() >= 2 {
return values[0].to_string() +
"到" +
values[values.length() - 1].to_string()
}
}
if values.length() > 1 {
let step = values[1] - values[0]
let mut is_step = step > 1
let mut i = 1
while i < values.length() - 1 && is_step {
if values[i + 1] - values[i] != step {
is_step = false
}
i = i + 1
}
if is_step && step > 1 {
return "每" +
step.to_string() +
"个从" +
values[0].to_string() +
"到" +
values[values.length() - 1].to_string()
}
}
let mut result = ""
for j = 0; j < values.length(); j = j + 1 {
if j > 0 {
result = result + "、"
}
result = result + values[j].to_string()
}
result
}
///|
/// Describe values in Chinese, using name mappings where available.
fn describe_values_cn_names(
values : Array[Int],
names : Array[String],
) -> String {
if values.length() == 0 {
return ""
}
if is_consecutive_range(values) {
if values.length() >= 2 {
let first = format_value_cn(values[0], names)
let last = format_value_cn(values[values.length() - 1], names)
return first + "到" + last
}
}
let mut result = ""
for j = 0; j < values.length(); j = j + 1 {
if j > 0 {
result = result + "、"
}
result = result + format_value_cn(values[j], names)
}
result
}
///|
/// Format a value for Chinese display, using name mapping if available.
fn format_value_cn(value : Int, names : Array[String]) -> String {
if value >= 0 && value < names.length() && names[value] != "" {
names[value]
} else {
value.to_string()
}
}
///|
/// Join an array of strings with a separator.
fn join_strings(parts : Array[String], sep : String) -> String {
let mut result = ""
for i = 0; i < parts.length(); i = i + 1 {
if i > 0 {
result = result + sep
}
result = result + parts[i]
}
result
}
///|
/// Find slash position in string.
fn find_slash_pos(s : String) -> Int {
for i = 0; i < s.length(); i = i + 1 {
if s[i].to_int() == 47 {
return i
}
}
-1
}
///|
/// Parse a strict integer (empty string returns None).
fn parse_int_desc(s : String) -> Int? {
if s == "" {
return None
}
let mut value = 0
for i = 0; i < s.length(); i = i + 1 {
let code = s[i].to_int()
if code < 48 || code > 57 {
return None
}
value = value * 10 + (code - 48)
}
Some(value)
}