Community Blog
Get the latest updates on the Splunk Community, including member experiences, product education, events, and more!

[Puzzles] Solve, Learn, Repeat: Matching cron expressions

ITWhisperer
SplunkTrust
SplunkTrust

This puzzle (first published here) is based on matching timestamps to cron expressions.

All the timestamps follow this pattern:

YYYY-MM-DDTHH:mm:ss​

Given that there are a number of interpretations/implementations of cron, it is worth specifying that the definition used in this puzzle follows this basic pattern:

  * * * * * <command to execute>
# | | | | |
# | | | | day of the week (0–6) (Sunday to Saturday)
# | | | month (1–12)
# | | day of the month (1–31)
# | hour (0–23)
# minute (0–59)​

Short names, ranges, step values and lists are also supported. Wikipedia has a good description of this here.

For example, the input looks something like this:

2025-10-23T18:31:23
2026-01-01T16:25:39

* 0 * * * command
* * * 4 * command​

The first part of the input (before the blank line) are timestamps (imagine they have been retrieved from some log entries).

The second part of the input (after the blank line) are some user-level (not system-level) crontab entries.

Since this version of cron does not support years or seconds, these can be assumed to match when matching timestamps.

The challenge is to determine which timestamps do not correlate with any crontab entries (2), and which crontab entries do not correlate to any timestamps (2).

This is intended to be a regular expression and SPL challenge, i.e. convert the cron expressions to regular expressions in order to find matching timestamps, although you could just use SPL if you like!

The input can be found here.

This article contains spoilers!

In fact, the whole article is a spoiler as it contains solutions to the puzzle. If you are trying to solve the puzzle yourself and just want some pointers to get you started, stop reading when you have enough, and return if you get stuck again, or just want to compare your solution to mine!

Analyse the Data

The first thing to notice is that all the timestamps follow the same format, and the cron expressions follow the basic pattern with a few extras as mentioned. This means that you do not need to validate the values per se, but you will need to understand what the cron expressions mean, in order to find matching timestamps.

Splitting into Timestamps and cron Expressions

The first thing to do is to split the large string into separate timestamps and cron expressions. Depending on how you set your data up, you may have been able to do this when it was ingested, or you could have just pasted it into a field (as I have done), and then split it using rex.

| makeresults
| fields - _time
| eval _raw="2025-10-23T18:31:23
2026-01-01T16:25:39
...
* 0 * * * command
* * * 4 * command"
| rex max_match=0 "(?<datetime>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})"
| rex max_match=0 "(?<cron>(\S+ ){5}.*)"
| mvexpand datetime
| streamstats count as datetimeentry
| mvexpand cron
| table datetimeentry datetime cron

Note the ellipsis to signify missing data, you can get the full input data from the link shown earlier. Note that I have also given each timestamp entry an id number so that we can reconstruct the order later (this is entirely optional).

This regular expression assumes that a "timestamp" is taken from any line that follows the timestamp format shown earlier, and the cron expressions follow the basic format shown earlier. Note that the rex expressions are also validating the input, i.e. anything that does not match either expression will not be picked up. At this point, it is worth checking that the timestamps and cron expressions look complete and accurate.

Part One

The first part of the puzzle is to determine which timestamps are not matched by any of the cron expressions. (The second part will be trivial after this!)

The approach I have taken (as hinted at in the puzzle definition), is to convert the cron expressions into regular expressions, and use those to look for matches with the timestamps.

Time components

Parse the cron expression to separate the different time components.

| rex field=cron "^(?<minute>\S+)\s(?<hour>\S+)\s(?<dom>\S+)\s(?<month>\S+)\s(?<dow>\S+)"

Dealing with minutes

Since lists are allowed, expand any list present in the minute field.

| eval minute=split(minute,",")
| mvexpand minute

Since ranges are also allowed, split out any ranges.

| eval minute_range=coalesce(split(minute,"-"), minute)

If minute_range has more than one part in the multi-part field, it must represent a range, so, convert it to a multi-value field with all the represented values, and expand it.

| eval minute=if(mvcount(minute_range) > 1, mvrange(tonumber(mvindex(minute_range,0)), tonumber(mvindex(minute_range,1))+1), minute)
| fields - minute_range
| mvexpand minute

Note the use of the tonumber() function. This is because the mvindex() function returns strings, even if the multi-value field contains just numbers!

Step values are also allowed, so we need to deal with those if they exist.

| eval minute_recurring=coalesce(split(minute,"/"), minute)

For step values, the first part is the base.

| eval minute_base=if(mvcount(minute_recurring) > 1, mvindex(minute_recurring,0), null())
| eval minute_base=if(minute_base  = "*", 0, tonumber(minute_base))

Looking at the data, you may have noticed that all the step values have "*" as the base, so these two lines can be compressed to:

| eval minute_base=if(mvcount(minute_recurring) > 1, 0, null())

The interval is the second part (if present) is the interval that the base is incremented by (until the maximum is reached).

| eval minute_interval=if(mvcount(minute_recurring) > 1, tonumber(mvindex(minute_recurring,1)), null())

Given that the base (if step values are being used) is always "*" (in the full data), determine how many times the interval can be repeated 

| eval minute_repeats=if(mvcount(minute_recurring) > 1, 60 / tonumber(mvindex(minute_recurring,1)), null())

Note that all the minute step values in the data are factors of 60, so this calculation results in the correct number of repeats.

Expand the number of repeats, and calculate the value of the minute for each repeat.

| eval minute_repeats=if(mvcount(minute_recurring) > 1, mvrange(0,minute_repeats), "")
| mvexpand minute_repeats
| eval minute=if(minute_repeats = "", minute, (minute_base + (minute_repeats * minute_interval)) % 60)
| fields - minute_repeats minute_interval minute_base minute_recurring

Format the minute value to match the format used in the timestamp.

| eval minute=if(minute="*", minute, printf("%0.2d", minute))

Dealing with hours and day of the month

The same sort of processing can be applied to hours and day of the month with the maximum being 24 and 31 respectively instead of 60.

Note that, since the maximum for day of the month is a prime number, a slightly different calculation has to be done to determine the number of repetitions required.

| eval dom_repeats=if(mvcount(dom_recurring) > 1, ceil(31 / tonumber(mvindex(dom_recurring,1))), null())

Bear in mind that days are numbered from one not zero, so zero should be corrected to 31.

| eval dom=if(dom = 0, 31, dom)

Dealing with months

Months can be expressed as short names or numerically. As we have ranges to deal with, convert the names to numbers.

| eval month=split(month,",")
| mvexpand month
| rex mode=sed field=month "s!(?i)jan!01!g s!(?i)feb!02!g s!(?i)mar!03!g s!(?i)apr!04!g s!(?i)may!05!g s!(?i)jun!06!g s!(?i)jul!07!g s!(?i)aug!08!g s!(?i)sep!09!g s!(?i)oct!10!g s!(?i)nov!11!g s!(?i)dec!12!g"

Now deal with ranges and step values as before (maximum month value is 12).

Numeric months are numbered from one so zero should be converted to twelve.

| eval month_base=if(mvcount(month_recurring) > 1, 1, null())

and

| eval month=if(month_repeats = "", month, (month_base + (month_repeats * month_interval)) % 12)
| eval month=if(month = 0, 12, month)

Dealing with days of the week

Days of the week can be expressed as short names or numerically. As we have ranges to deal with, convert the names to numbers.

| eval dow=split(dow,",")
| mvexpand dow
| rex mode=sed field=dow "s!(?i)sun!0!g s!(?i)mon!1!g s!(?i)tue!2!g s!(?i)wed!3!g s!(?i)thu!4!g s!(?i)fri!5!g s!(?i)sat!6!g"

Now deal with ranges and step values as before (there are 7 days numbered 0 to 6) and since 7 is a prime number, a slightly different calculation has to be done to determine the number of repetitions required.

| eval dow_repeats=if(mvcount(dow_recurring) > 1, ceil(7 / tonumber(mvindex(dow_recurring,1))), null())

Convert the day of the week number (back) to a short name.

| eval dow=case(dow=0, "Sun", dow=1, "Mon", dow=2, "Tue", dow=3, "Wed", dow=4, "Thu", dow=5, "Fri", dow=6, "Sat", true(), dow)

Reassemble the time components

For each cron expression in each timestamp, gather the options for the 5 time components.

| stats values(month) as month values(dom) as dom values(hour) as hour values(minute) as minute values(dow) as dow by datetimeentry datetime cron

Generate regular expression

To convert each time component to a regular expression, consider what each might be; it will either be an asterisk (*), a single value, or a multi-value. For each case, the corresponding regular expression will be two digits or three letters, the value, or a pipe-delimited (|) set of alternative values.

Starting with hours:

| eval cron_datetime=if(hour="*","[0-9][0-9]",if(mvcount(hour) > 1, "(".mvjoin(hour, "|").")", hour))." "

Where there is more than one option, surround the pipe-delimited set of alternative values with brackets (). Continue with the other time components.

| eval cron_datetime=cron_datetime.if(minute="*","[0-9][0-9]",if(mvcount(minute) > 1, "(".mvjoin(minute, "|").")", minute))." "
| eval cron_datetime=cron_datetime.if(dom="*", "[0-9][0-9]", if(mvcount(dom) > 1, "(".mvjoin(dom, "|").")", dom))." "
| eval cron_datetime=cron_datetime.if(month="*","[0-9][0-9]",if(mvcount(month) > 1, "(".mvjoin(month, "|").")", month))." "
| eval cron_datetime=cron_datetime.if(dow="*", "[A-Za-z]{3}", if(mvcount(dow) > 1, "(".mvjoin(dow, "|").")", dow))

The regular expression is in cron time component order; in order to compare with the timestamps, convert them to the same order.

| eval datetimeday=strftime(strptime(datetime,"%FT%T"),"%H %M %d %m %a")

Note the use of the "%a" component which provides the name of the day of the week which is not present in the input timestamps.

Make a copy of the original timestamp is the converted timestamp matches the regular expression version of the cron expression.

| eval match=if(match(datetimeday,cron_datetime),datetime,"")

Gather the cron expressions for each timestamp grouped by whether they match or not.

| stats values(cron) as cron by datetimeentry datetimeday match

Now, compress the matches so there is only one event per timestamp.

| stats values(match) as matches by datetimeentry datetimeday

Finally, sort by entry id to return to the original order.

| sort 0 datetimeentry

Is this correct?

There should only be two timestamps which do not match any of the cron expressions, but there are five. So, what have we missed. Since there are more timestamps which do not match cron expressions than there should be, this implies that one or more cron expressions do not match any timestamps when they should. But, which cron expression or expressions?

Part Two

Perhaps the easiest way to find out is to complete Part Two and see if that provides any clues.

As indicated earlier, this is reasonably trivial. All that is needed is to change the stats and sort commands after the matching is done.

| eval match=if(match(datetimeday,cron_datetime),datetime,"")
| stats values(eval(if(match="",null(),match))) as used by cronentry cron
| sort 0 cronentry

This gives three expressions instead of the two expected, so it is likely to be one of these which has been incorrectly handled. Of these, the one which sticks out is this:

* * 3 * 0,2 command

On closer examination of the cron description, notice that, for day of the month and day of the week, when they are restricted i.e. not wildcards (*), only one has to match (although both could match). This means that the regular expression needs to take this into account.

If the regular expression is reordered to put these time components next to each other (and reformat the timestamp accordingly), the regular expression can be built like this:

| eval cron_datetime=if(hour="*","[0-9][0-9]",if(mvcount(hour) > 1, "(".mvjoin(hour, "|").")", hour))." "
| eval cron_datetime=cron_datetime.if(minute="*","[0-9][0-9]",if(mvcount(minute) > 1, "(".mvjoin(minute, "|").")", minute))." "
| eval cron_datetime=cron_datetime.if(month="*","[0-9][0-9]",if(mvcount(month) > 1, "(".mvjoin(month, "|").")", month))." "
| eval cron_datetime=cron_datetime.if(dom="*", if(dow="*", "[0-9][0-9] [A-Za-z]{3}", "[0-9][0-9] ".if(mvcount(dow) > 1, "(".mvjoin(dow, "|").")", dow)), if(dow="*", if(mvcount(dom) > 1, "(".mvjoin(dom, "|").")", dom)." [A-Za-z]{3}", "(".if(mvcount(dom) > 1, "(".mvjoin(dom, "|").")", dom)." [A-Za-z]{3})|([0-9][0-9] ".if(mvcount(dow) > 1, "(".mvjoin(dow, "|").")", dow).")"))
| eval datetimeday=strftime(strptime(datetime,"%FT%T"),"%H %M %m %d %a")

This now gives two expressions which have no matching timestamps (as required).

Plugging this change into Part One, give two timestamps which have no matching cron expressions (as required).

Summary

In summary, creating regular expressions on the fly can be a useful way to compare different fields. Also, checking your results to ensure that they match the expected results, is invaluable to ensuring you have a correct and robust solution. This means that you may need to go back to the source data and/or specification to ensure that you fully understand what you are dealing with.

Have questions or thoughts? Comment on this article or in Slack #puzzles channel. Whichever you prefer.

inventsekar
SplunkTrust
SplunkTrust

Dear @ITWhisperer Sir, Splendid work, as usual. 
(the last last months posts i missed and just yesterday i saw all those and i am working on all those puzzles one by one, planning to make a video for my YouTube channel, with special mention to you as the author).

can i post the solutions here? (i mean, thinking about to keep the suspense for other learners), pls suggest, thanks. 


----------------------------------------------------------------------------------------------
If this post or any post addressed your question, could you pls:
Give it karma to show appreciation

PS - As of May 2026, my Karma Given is 2312 and my Karma Received is 497, lets revamp the Karma Culture!
Thanks and best regards, Sekar
--------------------------------------------------------------------------------------------

ITWhisperer
SplunkTrust
SplunkTrust

Dear @inventsekar ,

Posting solutions (or even partial solutions / spoilers) here or in the Slack #puzzles channel would be great to see. There are often many ways to solve / approach these puzzles (and my way may not be the best or most efficient!), and one of the purposes of this series of puzzles is to showcase different techniques that can be re-used and applied to real-world usecases that Splunk users need to solve, although there is some element of just the pure joy of cracking a problem, even if it doesn't directly lead to tangible benefits.

If you can fit your solution into a comment, that would be good, although I have found that there is a limit to how big these can be, so you may need to spread across multiple comments. Also, posting in comments (rather than Slack) may give you better options when it comes to formatting.

Have fun with the rest of the puzzles, and do post a link (or links) to your YouTube videos.

Contributors
Get Updates on the Splunk Community!

Splunk Community Badges!

  Hey everyone! Ready to earn some serious bragging rights in the community? Along with our existing badges ...

How to find the worst searches in your Splunk environment and how to fix them

Everyone knows Splunk is a powerful platform for running searches and doing data analytics. Your ...

Share Your Feedback: On Admin Config Service (ACS)!

Help Us Build a Better Admin Config Service Experience (ACS)   We Want Your Feedback on Admin Config Service ...