Cron library for Zig

Cron is a parser library for crontab schedule entries and determining the next/prev execution time. The basic functionality has been completed, and a fuzz test is scheduled to run daily on GitHub Actions to verify its correctness.

Basic API

// create a cron object
var c = Cron.init();
// parse cron entry
try c.parse("5 4 * * *");
// Get next execution time from `now`
const next = c.next(now);

Simple Scheduler Example

const std = @import("std");
const Cron = @import("cron").Cron;
const datetime = @import("datetime").datetime;

fn job1(i: usize) !void {
    const now = datetime.Datetime.now();

    var buf: [64]u8 = undefined;
    const dt_str = try now.formatISO8601Buf(&buf, false);
    std.log.info("{s} {d}th execution", .{ dt_str, i });
}

pub fn main() !void {
    var c = Cron.init();
    // At every minute.
    try c.parse("*/1 * * * *");

    for (0..5) |i| {
        const now = datetime.Datetime.now();

        // Get the next run time
        const next_dt = try c.next(now);
        const duration = next_dt.sub(now);
        // convert to nanoseconds
        const nanos = duration.totalSeconds() * std.time.ns_per_s + duration.nanoseconds;

        // wait next
        std.time.sleep(@intCast(u64, nanos));

        try job1(i + 1);
    }
}

Currently, it depends on zig-datetime to handle time. I’m considering exposing a set of interfaces with timestamp parameters. Feedback is welcome.

5 Likes

Cool! Thanks for sharing your work!

Have you considered moving the parse function’s string argument to comptime? I’m writing a comptime parser for strings to do einsum tensor operations and Zig has really made that easy to do.

Do you see most arguments being passed to the parse function as being something specified by the user directly or as something that can only be runtime determined? I don’t know enough about your design to be able to say from this vantage point.

1 Like

In some scenarios, parameters can be determined directly at compile-time. However, in other scenarios, such as reading a file like “/etc/crontab” at runtime, it depends on how the user chooses.

1 Like

Okay! So I see that this can have some file system integration then (makes sense now that you’ve said that).

So help me understand the structure of your project a bit more. I read the lib file and it seems to me that the lib file is currently for running tests (such as “test normal”, etc). This may be nitpicking, but I tend to think that lib (as it’s normally defined) is where implementations are either dynamically or statically linked in. Your cron.zig file is where the main cron struct is found but it does not depend at all on lib as a file - currently the import structure is:

const std = @import("std");
const log = std.log;
const datetime = @import("datetime").datetime;
const ext = @import("./datetime.zig");

const Error = @import("./error.zig").Error;
const isDigit = @import("./utils.zig").isDigit;
const CronExpr = @import("./expr.zig").CronExpr;
const CronField = @import("./expr.zig").CronField;
const FieldTag = @import("./expr.zig").FieldTag;

I would try to place those tests closer to their implementation files. The reason is for cascading tests. If I test a zig file itself, it will then test all of the dependent files upstream. Since lib is not upstream of cron.zig, it would seem like those tests need to be run independently (by testing the lib file alone). Thus, lib needs to be run independently of the main file in the current state? Any reason for that?

Yes, you are correct. Tests should be placed closer to their implementations to improve clarity. The reason I structured it this way is because the build.zig generated by zig init-lib uses lib.zig as the root file.

    // Creates a step for unit testing. This only builds the test executable
    // but does not run it.
    const main_tests = b.addTest(.{
        .root_source_file = .{ .path = "src/lib.zig" },
        .target = target,
        .optimize = optimize,
    });

    const run_main_tests = b.addRunArtifact(main_tests);

    // This creates a build step. It will be visible in the `zig build --help` menu,
    // and can be selected like this: `zig build test`
    // This will evaluate the `test` step rather than the default, which is "install".
    const test_step = b.step("test", "Run library tests");
    test_step.dependOn(&run_main_tests.step);

It seems that I should follow the approach used in std.zig by adding testing.refAllDecls(@This()); in lib.zig, and then move the tests to separate modules.

1 Like