Repository: bradymholt/cRonstrue
Pull request #350 (2025-05-05 03:51:22)

Add support for @reboot special syntax
## Summary
- Added support for the missing `@reboot` syntax, which is commonly used in cron implementations
- Added `@reboot` to the list of supported special expressions in the CronParser class
- Created a special handler to return "At system startup" for @reboot in the ExpressionDescriptor
- Added comprehensive tests for the new functionality
- Updated README to document the @reboot support

## Test plan
- Added unit tests for both CronParser and ExpressionDescriptor classes
- Manual testing with various expressions to ensure compatibility with existing features

kylesnowschwartz: Converted to draft while I validate that this works locally
kylesnowschwartz: I rebased on the latest main so I could have the CLI changes to support @syntax working for further validation :D 

Looks good to me now @bradymholt thanks for your review
bradymholt: Thanks!

Patch:
diff --git a/README.md b/README.md
index 6985c67..2649a5e 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@ This library was ported from the original C# implementation called [cron-express
 - Supports all cron expression special characters including * / , - ? L W, #
 - Supports 5, 6 (w/ seconds or year), or 7 (w/ seconds and year) part cron expressions
 - [Quartz Job Scheduler](http://www.quartz-scheduler.org/) cron expressions are supported
+- Supports time specification _nicknames_ (@yearly, @annually, @monthly, @weekly, @daily, @reboot)
 - i18n support with 30+ languages
 
 ## Demo
@@ -94,6 +95,9 @@ cronstrue.toString("* * * 6-8 *", { monthStartIndexZero: true });
 cronstrue.toString("@monthly");
 > "At 12:00 AM, on day 1 of the month"
 
+cronstrue.toString("@reboot");
+> "Run once, at startup"
+
 ```
 
 For more usage examples, including a demonstration of how cRonstrue can handle some very complex cron expressions, you can [reference the unit tests](https://github.com/bradymholt/cRonstrue/blob/main/test/cronstrue.ts).
diff --git a/src/cronParser.ts b/src/cronParser.ts
index 79057fb..6fafd21 100644
--- a/src/cronParser.ts
+++ b/src/cronParser.ts
@@ -29,10 +29,13 @@ export class CronParser {
 
     var expression = this.expression ?? '';
 
-    if (expression.startsWith('@')) {
+    if (expression === "@reboot") {
+      // Special handling for @reboot - create a marker array with a special format
+      parsed = ["@reboot", "", "", "", "", "", ""];
+      return parsed;
+    } else if (expression.startsWith('@')) {
       var special = this.parseSpecial(this.expression);
       parsed = this.extractParts(special);
-
     } else {
       parsed = this.extractParts(this.expression);
     }
@@ -51,7 +54,8 @@ export class CronParser {
         '@weekly': '0 0 * * 0',
         '@daily': '0 0 * * *',
         '@midnight': '0 0 * * *',
-        '@hourly': '0 * * * *'
+        '@hourly': '0 * * * *',
+        '@reboot': '@reboot'
     };
 
     const special = specialExpressions[expression];
@@ -328,7 +332,6 @@ export class CronParser {
     this.validateOnlyExpectedCharactersFound(parsed[6], standardCronPartCharacters);
 
     this.validateAnyRanges(parsed);
-
   }
 
   protected validateAnyRanges(parsed: string[]) {
diff --git a/src/expressionDescriptor.ts b/src/expressionDescriptor.ts
index 0a6f9c1..37954dc 100644
--- a/src/expressionDescriptor.ts
+++ b/src/expressionDescriptor.ts
@@ -104,6 +104,12 @@ export class ExpressionDescriptor {
         this.options.monthStartIndexZero
       );
       this.expressionParts = parser.parse();
+
+      // Special handling for @reboot
+      if (this.expressionParts[0] === "@reboot") {
+        return this.i18n.atReboot?.() || "Run once, at startup";
+      }
+
       var timeSegment = this.getTimeOfDayDescription();
       var dayOfMonthDesc = this.getDayOfMonthDescription();
       var monthDesc = this.getMonthDescription();
diff --git a/src/i18n/locale.ts b/src/i18n/locale.ts
index 935ae34..35b6662 100644
--- a/src/i18n/locale.ts
+++ b/src/i18n/locale.ts
@@ -80,4 +80,5 @@ export interface Locale {
    * @return {string[]} months of year
    */
   monthsOfTheYearInCase?(f?: number): string[];
+  atReboot?(): string;
 }
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index dba4fbd..0156c99 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -176,4 +176,8 @@ export class en implements Locale {
       "December",
     ];
   }
+
+  atReboot() {
+    return "Run once, at startup";
+  }
 }


Test patch:
diff --git a/test/cronParser.ts b/test/cronParser.ts
index f630cc0..96a172b 100644
--- a/test/cronParser.ts
+++ b/test/cronParser.ts
@@ -61,5 +61,11 @@ describe("CronParser", function () {
     it("should parse cron @ expression", function () {
       assert.equal(new CronParser("@weekly").parse().length, 7);
     });
+
+    it("should parse @reboot expression", function () {
+      const parsedReboot = new CronParser("@reboot").parse();
+      assert.equal(parsedReboot.length, 7);
+      assert.equal(parsedReboot[0], "@reboot");
+    });
   });
 });
diff --git a/test/cronstrue.ts b/test/cronstrue.ts
index 341940b..6b1491b 100644
--- a/test/cronstrue.ts
+++ b/test/cronstrue.ts
@@ -719,6 +719,10 @@ describe("Cronstrue", function () {
     it("@hourly", function () {
       assert.equal(cronstrue.toString(this.test?.title as string), "Every hour");
     });
+
+    it("@reboot", function () {
+      assert.equal(cronstrue.toString(this.test?.title as string), "Run once, at startup");
+    });
   });
 
   describe("verbose", function () {