Phy Cron Explainer

TotalClaw 作者 PHY041 v1.0.0

Cron 表达式解释器、验证器和转换器。将任何 cron 表达式翻译成简单的英语(“0 */6 * * *”→“每 6 小时,第 0 分钟”),显示接下来 5 个计划的运行时间以及确切的日期,检测不可能/冲突的表达式(2 月 30 日,冲突的星期 + 月份),识别可疑模式(生产中的每一分钟,昂贵操作的非常频繁的计划),并将简单的英语描述转换为 cron(“每个工作日上午 9 点”→“0 9 * * 1-5”)。支持所有主要风格:标准 Unix 5 字段、带秒的 6 字段、AWS EventBridge、GitHub Actions 计划、Kubernetes CronJob、Spring @Scheduled 和特殊字符串(@yearly、@monthly、@daily、@hourly、@reboot)。还扫描 .github/workflows/ 和 Kubernetes YAML 文件以获取 cron 计划并对其进行审核。零外部API——纯本地解析。触发“cron 表达式”、“这个 cron 是什么意思”、“cron 到英语”、“cron 计划”、“解释 cron”、“@reboot”、“/cron”。

源码 ↗

安装 / 下载方式

TotalClaw CLI推荐
totalclaw install totalclaw:phy041~phy-cron-explainer
cURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/totalclaw%3Aphy041~phy-cron-explainer/file -o phy-cron-explainer.md
Git 仓库获取源码
git clone https://github.com/openclaw/skills/commit/bc809f995e64b79ce05b5c106095661309af6682
## 概述(中文)

Cron 表达式解释器、验证器和转换器。将任何 cron 表达式翻译成简单的英语(“0 */6 * * *”→“每 6 小时,第 0 分钟”),显示接下来 5 个计划的运行时间以及确切的日期,检测不可能/冲突的表达式(2 月 30 日,冲突的星期 + 月份),识别可疑模式(生产中的每一分钟,昂贵操作的非常频繁的计划),并将简单的英语描述转换为 cron(“每个工作日上午 9 点”→“0 9 * * 1-5”)。支持所有主要风格:标准 Unix 5 字段、带秒的 6 字段、AWS EventBridge、GitHub Actions 计划、Kubernetes CronJob、Spring @Scheduled 和特殊字符串(@yearly、@monthly、@daily、@hourly、@reboot)。还扫描 .github/workflows/ 和 Kubernetes YAML 文件以获取 cron 计划并对其进行审核。零外部API——纯本地解析。触发“cron 表达式”、“这个 cron 是什么意思”、“cron 到英语”、“cron 计划”、“解释 cron”、“@reboot”、“/cron”。

## 原文

# Cron Explainer

`0 2 * * 1`

What does that mean? Every Monday at 2am. How about `*/5 9-17 * * 1-5`? Every 5 minutes between 9am and 5pm on weekdays.

You shouldn't need to count fields in your head. This skill translates any cron expression into plain English, shows the next scheduled runs, catches impossible expressions before they go to production, converts English descriptions to cron, and scans your CI/K8s configs to audit all your schedules in one shot.

**All major cron flavors. Zero external API.**

---

## Trigger Phrases

- "cron expression", "explain this cron", "what does cron mean"
- "cron to English", "translate cron"
- "cron schedule", "next run time"
- "English to cron", "convert to cron"
- "cron syntax", "cron debug"
- "kubernetes cronjob", "github actions schedule"
- "/cron"

---

## How to Provide Input

```bash
# Option 1: Explain a cron expression
/cron "0 2 * * 1"
/cron "*/5 9-17 * * 1-5"
/cron "0 0 1,15 * *"

# Option 2: Explain a special string
/cron "@daily"
/cron "@reboot"
/cron "@every 6h"    # Kubernetes/Go format

# Option 3: Convert English to cron
/cron "every weekday at 9am"
/cron "every 5 minutes during business hours"
/cron "first day of every month at midnight"
/cron "every 15 minutes from 8am to 6pm on weekdays"

# Option 4: Show next N run times
/cron "0 */6 * * *" --next 10

# Option 5: Validate an expression
/cron "0 25 * * *" --validate    # 25th hour — invalid

# Option 6: Scan project files for cron schedules
/cron --scan .                    # scans GitHub Actions, Kubernetes YAML, crontab
/cron --scan .github/workflows/

# Option 7: Detect flavor
/cron "0/5 * * * ? *" --flavor aws-eventbridge
```

---

## Step 1: Parse and Explain Cron Expression

```python
from datetime import datetime, timedelta
import re
from typing import Optional


# ── Special strings ──────────────────────────────────────────────────────────

SPECIAL_STRINGS = {
    '@yearly':   ('0 0 1 1 *',  'Once a year, at midnight on January 1st'),
    '@annually': ('0 0 1 1 *',  'Once a year, at midnight on January 1st'),
    '@monthly':  ('0 0 1 * *',  'Once a month, at midnight on the 1st'),
    '@weekly':   ('0 0 * * 0',  'Once a week, at midnight on Sunday'),
    '@daily':    ('0 0 * * *',  'Every day at midnight'),
    '@midnight': ('0 0 * * *',  'Every day at midnight'),
    '@hourly':   ('0 * * * *',  'Every hour, at the top of the hour'),
    '@reboot':   (None,         'Once, on system startup/reboot'),
    '@every_1m': ('* * * * *',  'Every minute'),
    '@every_5m': ('*/5 * * * *','Every 5 minutes'),
    '@every_1h': ('0 * * * *',  'Every hour'),
}

WEEKDAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday',
                 'Thursday', 'Friday', 'Saturday']
MONTH_NAMES = ['January', 'February', 'March', 'April', 'May', 'June',
               'July', 'August', 'September', 'October', 'November', 'December']


def detect_flavor(expression: str) -> str:
    """Detect cron flavor from expression structure."""
    parts = expression.strip().split()
    if len(parts) == 6:
        # Could be 6-field with seconds (Quartz/Spring) or 6-field Linux
        if re.search(r'[?L#W]', parts[5]):
            return 'quartz'
        return '6field-seconds'
    if len(parts) == 7:
        return 'quartz-7field'
    if len(parts) == 5:
        return 'standard'
    return 'unknown'


def explain_field(value: str, field: str, names: Optional[list] = None) -> str:
    """Convert a single cron field to English."""

    if value == '*':
        return None  # "every" is implied

    if value == '?':
        return None  # Quartz "any" — implied

    # Step: */N or start/N
    step_match = re.match(r'^(\*|\d+)/(\d+)$', value)
    if step_match:
        start = step_match.group(1)
        step = int(step_match.group(2))
        if field == 'minute':
            return f'every {step} minute{"s" if step > 1 else ""}'
        if field == 'hour':
            return f'every {step} hour{"s" if step > 1 else ""}'
        if field == 'day_of_month':
            return f'every {step} day{"s" if step > 1 else ""}'
        if field == 'month':
            return f'every {step} month{"s" if step > 1 else ""}'
        if field == 'second':
            return f'every {step} second{"s" if step > 1 else ""}'
        return f'every {step}'

    # Range: N-M
    range_match = re.match(r'^(\d+)-(\d+)$', value)
    if range_match:
        lo, hi = int(range_match.group(1)), int(range_match.group(2))
        if names:
            lo_name = names[lo] if lo < len(names) else str(lo)
            hi_name = names[hi] if hi < len(names) else str(hi)
            return f'{lo_name} through {hi_name}'
        if field == 'hour':
            lo_fmt = f'{lo}:00 {"AM" if lo < 12 else "PM"}'
            hi_fmt = f'{hi % 12 or 12}:00 {"AM" if hi < 12 else "PM"}'
            return f'between {lo_fmt} and {hi_fmt}'
        return f'{lo} to {hi}'

    # Range with step: N-M/S
    range_step_match = re.match(r'^(\d+)-(\d+)/(\d+)$', value)
    if range_step_match:
        lo = int(range_step_match.group(1))
        hi = int(range_step_match.group(2))
        step = int(range_step_match.group(3))
        range_desc = explain_field(f'{lo}-{hi}', field, names)
        return f'every {step} {field.replace("_", " ")}{"s" if step > 1 else ""} {range_desc}'

    # List: N,M,P
    if ',' in value:
        parts = [p.strip() for p in value.split(',')]
        if names:
            named = [names[int(p)] if p.isdigit() and int(p) < len(names) else p for p in parts]
        else:
            named = parts
        if len(named) == 2:
            return f'{named[0]} and {named[1]}'
        return ', '.join(named[:-1]) + f', and {named[-1]}'

    # Single value
    if value.isdigit():
        n = int(value)
        if field == 'hour':
            if n == 0: return 'midnight'
            if n == 12: return 'noon'
            if n < 12: return f'{n}:00 AM'
            return f'{n - 12}:00 PM'
        if field == 'minute':
            return f'minute {n}' if n != 0 else 'on the hour'
        if field == 'second':
            return f'second {n}'
        if field == 'day_of_month':
            suffix = 'st' if n % 10 == 1 and n != 11 else \
                     'nd' if n % 10 == 2 and n != 12 else \
                     'rd' if n % 10 == 3 and n != 13 else 'th'
            return f'the {n}{suffix}'
        if field == 'month' and names:
            return names[n - 1] if 1 <= n <= 12 else str(n)
        if field == 'day_of_week' and names:
            return names[n % 7]
        return str(n)

    # Special Quartz: L (last), W (nearest weekday), # (Nth weekday)
    if value.upper() == 'L' and field == 'day_of_month':
        return 'the last day of the month'
    if value.upper() == 'L' and field == 'day_of_week':
        return 'the last weekday of the month'
    lw_match = re.match(r'^(\d+)W$', value, re.I)
    if lw_match:
        return f'the weekday nearest the {lw_match.group(1)}th'
    hash_match = re.match(r'^(\d+)#(\d+)$', value)
    if hash_match:
        day = int(hash_match.group(1))
        nth = int(hash_match.group(2))
        ordinals = ['', '1st', '2nd', '3rd', '4th', '5th']
        day_name = WEEKDAY_NAMES[day % 7]
        return f'the {ordinals[nth]} {day_name} of the month'

    return value  # fallback — return as-is


def explain_cron(expression: str) -> dict:
    """
    Parse a cron expression and return a human-readable explanation.
    Returns dict with: explanat