📖 Table of Contents
🔗 You can find the full source code for Occurify on GitHub.
Introduction
I’ve always found working with time in code way more complicated than it needs to be.
Whether it’s dealing with schedules, recurring events, or aligning actions across time zones, time-based logic quickly becomes messy, repetitive, and full of edge cases. I wanted a way to think about time more conceptually—to express ideas like “every sunset” or “weekdays between 9 and 5” without getting bogged down in hardcoded timestamps and brittle date math.
That’s why I built Occurify — a powerful and intuitive .NET library for defining, filtering, transforming, and scheduling time in a more human-friendly and composable way.
Occurify lets you break down time into clear building blocks: instants, periods, and timelines. It helps you reason about time the way you actually think about it—and then turns those ideas into clean, reliable code.
Whether you're working with time zones, cron expressions, solar events like sunrise and sunset, or even reactive scheduling, Occurify is designed to help you build smarter, more flexible time-driven systems.
In this article, I’ll walk you through:
- How Occurify models time using instants, periods, and timelines
- Defining dynamic and reusable time-based concepts (like "20 minutes after sunset")
- Filtering, transforming, and composing timelines with a fluent API
- Randomizing timing and filtering by patterns like weekdays or holidays
- Scheduling real-world actions using ReactiveX
- How the design of Occurify makes all these feats possible.
Usage
Instead of dealing with fixed timestamps, Occurify lets you think about time in a more human-friendly way. You don't need to precompute every possible event—just define the concept of an event, like "all sunsets," and let Occurify handle the rest.
As an example, let’s imagine we want to automate our lights to turn on in the evening.
Defining Timelines
Instead of manually maintaining a list of sunset times, we can simply use:
ITimeline sunsets = AstroInstants.LocalSunset;
This timeline now represents every sunset dynamically—no need for hardcoded schedules.
Transforming Timelines
Want to schedule events 20 minutes after sunset? Just shift the timeline:
ITimeline twentyMinAfterSunset = sunsets + TimeSpan.FromMinutes(20);
Now, twentyMinAfterSunset
dynamically represents every sunset, plus 20 minutes—no manual calculations needed.
Combining Timelines Into Periods
Now, let’s define a time when the lights should turn off and create a period from 20 minutes after sunset until 11 PM:
ITimeline elevenPm = TimeZoneInstants.DailyAt(hour: 23);
IPeriodTimeline lightOnPeriods = twentyMinAfterSunset.To(elevenPm);
With this, lightOnPeriods now represents all the evening periods when the lights should be on.
Filtering & Randomization
If you want the lights to turn on only on weekdays, you can filter the periods like this:
lightOnPeriods = lightOnPeriods.Within(TimeZonePeriods.Workdays());
To make the timing feel more natural, we can randomize the periods slightly by adding a variation of up to 10 minutes:
lightOnPeriods = lightOnPeriods.Randomize(TimeSpan.FromMinutes(10));
Using the Timeline
Checking if the Lights Should Be On Right Now
To check if the lights should be on at the current moment, you can simply use IsNow()
on the lightOnPeriods
timeline:
if (lightOnPeriods.IsNow()) {
// Turn lights on.
}
else {
// Turn lights off.
}
Enumerating Future (or Past) Events
You can easily enumerate future or past periods to check when the lights will go on. For example, let’s find out when the lights will turn on during the rest of the current month:
Console.WriteLine("The rest of the current month the lights will go on at:");
foreach (Period period in lightOnPeriods.EnumerateRange(DateTime.UtcNow, TimeZoneInstants.CurrentMonth().End!.Value)){
Console.WriteLine(period.Start);
}
But due to the dynamic nature of timelines we can just as easily see when the lights will turn on in February 2050:
Console.WriteLine("In February 2050 the lights will go on at:");
foreach (Period period in lightOnPeriods.EnumeratePeriod(TimeZonePeriods.Month(2, 2050))){
Console.WriteLine(period.Start);
}
Note that the period timeline only resolves the necessary periods when enumerated, ensuring efficiency.
Scheduling Automatic Actions
To automate actions based on the timeline, you can use ReactiveX, which provides a powerful way to handle event-driven programming. The SubscribeStartEnd
method internally utilizes an IObservable
, allowing you to schedule events reactively.
By default, this method even evaluates the current state of the timeline, invoking the applicable method on startup.
lightOnPeriods.SubscribeStartEnd(() => TurnLightsOn(), () => TurnLightsOff(), scheduler);
This approach allows you to focus on what matters—like defining when you want your lights to turn on—without manually handling the timing and scheduling. As a result, your code becomes more intuitive, dynamic, and use case-driven.
Curious about more use cases? Check out Occurify’s README on Github or explore the example code for more.
Design
Occurify uses 4 main concepts:
Concept | Represented by | Description |
---|---|---|
Instant | UTC DateTime
|
A single instant in time. |
Period | Period |
A period of time, defined by a start and end instant. |
Instant timeline | ITimeline |
A timeline containing instants. |
Period timeline | IPeriodTimeline |
A timeline containing periods. |
Instant
An instant is represented using a DateTime
with Kind
set to DateTimeKind.Utc
.
The valid range for an instant is from 01-01-0000
to 31-12-9999
.
Period
A period is defined by two instants: a start and an end.
- If the start is null, it means the period has no defined beginning (i.e., it started at the beginning of time).
- If the end is null, it means the period has no defined end (i.e., it lasts indefinitely).
- If both start and end are null, the period is infinite in both directions.
A period contains all instants that are greater than or equal to the start instant and smaller than the end instant:
(Start == null || instant >= Start) &&
(End == null || instant < End)
Key Concept: Consecutive periods (a period with the same start as the end of another) do not overlap, ensuring that each instant belongs to only one period.
Period Record:
record Period(DateTime? Start, DateTime? End) : IComparable<Period>
Different ways of creating a period:
DateTime utcNow = DateTime.UtcNow;
// Using extension methods
Period nowToOneHoursFromNow = utcNow.ToPeriodWithDuration(TimeSpan.FromHours(1));
Period nowToTwoHoursFromNow = utcNow.To(utcNow + TimeSpan.FromHours(2));
Period nowToNeverEnding = utcNow.To(null);
// Using static methods
Period nowToThreeHoursFromNow = Period.Create(utcNow, utcNow + TimeSpan.FromHours(3));
Instant Timeline
An instant timeline represents a timeline of instants.
Although an instant timeline implements IEnumerable
, it does not necessarily represent a collection of instants. Instead, it can represent the concept of a specific timeline. For example, an instant timeline can represent the concept of "all sunsets," without having to calculate or store all sunset times in memory. Instants in the timeline are only calculated when the timeline is enumerated or iterated over.
Within Occurify, a timeline has the following properties:
- Immutable: Once created, an instant timeline cannot be modified.
- Deterministic: The timeline will always yield the same instants given the same parameters.
-
Works with UTC: All instants in an instant timeline are represented as
DateTime
values in UTC (DateTimeKind.Utc
).
An instant timeline implements IEnumerable
, meaning that you can enumerate through it to access all instants, starting from the earliest to the latest.
Timeline Interface:
public interface ITimeline : IEnumerable<DateTime>
{
DateTime? GetPreviousUtcInstant(DateTime utcRelativeTo);
DateTime? GetNextUtcInstant(DateTime utcRelativeTo);
bool IsInstant(DateTime utcDateTime);
}
Different ways of creating an instant pipeline:
DateTime utcNow = DateTime.UtcNow;
// Using extension methods
ITimeline timeline1 = utcNow.AsTimeline();
ITimeline timeline2 = new[] { utcNow, utcNow + TimeSpan.FromHours(1) }.AsTimeline();
// Using static methods
ITimeline timeline3 = Timeline.FromInstants(utcNow, utcNow + TimeSpan.FromHours(1), utcNow, utcNow + TimeSpan.FromHours(3));
ITimeline timeline4 = Timeline.Periodic(TimeSpan.FromHours(1));
Note that timeline4
is not a timeline with concrete instants. Only when reading it, will the instants be resolved. Simular to Linq
methods, filtering only wraps the timeline in a filter class. Instants will only be resolved by reading.
Period Timeline
A period timeline is defined by a start timeline and an end timeline and represents a timeline of periods.
Periods on the period timeline start at any instant on the start timeline and end by the next first instant on the end timeline. Periods cannot overlap.
Due to the nature of instant timelines, period timelines can also represent a concept, but in the form of a period. For example, a period timeline can represent a concept like "all periods between sunrise and sunset", by constructing it from an instant timeline that represents "all sunrises" and one that represents "all sunsets".
If the earliest instant on both the start and end timelines is an end instant, the first period is assumed to have always started (start = null
). Simularly, if the last instant is a start instant, the last instant is assumed to never end (end = null
). If a period has no start and no end, it is empty. A period timeline cannot contain an infinite period.
If there are more consecutive start instants, the earliest one defines the start of a period. In case there are consecutive end instants, the earliest one defines the end of a period.
A period timeline also implements IEnumerable
. Enumerating a timeline will iterate all instants in a timeline from earliest to latest.
Period Timeline Interface:
public interface IPeriodTimeline : IEnumerable<Period>
{
ITimeline StartTimeline { get; }
ITimeline EndTimeline { get; }
}
Different ways of creating an period pipeline:
DateTime utcNow = DateTime.UtcNow;
Period period = utcNow.To(utcNow + TimeSpan.FromHours(2));
// Using extension methods on periods
IPeriodTimeline periodTimeline1 = period.AsPeriodTimeline();
IPeriodTimeline periodTimeline2 = new[] { period, period + TimeSpan.FromHours(2) }.AsPeriodTimeline();
// Using extension methods on instant timelines
ITimeline periodStartTimeline = Timeline.Periodic(TimeSpan.FromHours(1));
ITimeline periodEndTimeline = periodStartTimeline.OffsetMinutes(10);
IPeriodTimeline periodTimeline3 = periodStartTimeline.To(periodEndTimeline);
// Using static methods
IPeriodTimeline periodTimeline4 = PeriodTimeline.FromPeriods(period, period + TimeSpan.FromHours(2), period + TimeSpan.FromHours(4));
IPeriodTimeline periodTimeline5 = PeriodTimeline.Between(periodStartTimeline, periodEndTimeline);
Source Code
You can explore the full source code on GitHub.
If you find Occurify helpful or interesting, a ⭐️ on the repo would mean a lot!