ScheduleKit 1.0.0

dotnet add package ScheduleKit --version 1.0.0
                    
NuGet\Install-Package ScheduleKit -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ScheduleKit" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ScheduleKit" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="ScheduleKit" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ScheduleKit --version 1.0.0
                    
#r "nuget: ScheduleKit, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ScheduleKit@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ScheduleKit&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=ScheduleKit&version=1.0.0
                    
Install as a Cake Tool

ScheduleKit

Business hours, holiday calendars, and time calculations for SLA-aware scheduling in .NET.

Inspired by spatie/opening-hours and spatie/holidays.

NuGet CI


Table of Contents


Overview

ScheduleKit is a pure .NET calculation library with no storage dependencies. It gives you:

  • A fluent builder (BusinessSchedule) to define weekly opening hours per day of the week
  • A timezone-aware calculator (IBusinessScheduleCalculator) that adds business time, computes elapsed time, checks if a moment is open, and finds the next opening
  • A holiday system with built-in US federal holidays and full extensibility for any country or custom date
  • DI integration (IScheduleCalculator + IScheduleProvider) so you can load schedules from a database, config, or any source

All results are returned as UTC DateTimeOffset values. Schedules never touch a database — consumers own persistence.


Packages

Package Description
ScheduleKit Core library — schedule builder, calculator, holidays, DI extensions
ScheduleKit.AspNetCore ASP.NET Core helpers — scoped provider registration

Installation

dotnet add package ScheduleKit
# or for ASP.NET Core projects:
dotnet add package ScheduleKit.AspNetCore

Quick Start

using ScheduleKit.Builder;
using ScheduleKit.Calculation;
using ScheduleKit.Holidays;

// 1. Define a schedule
var schedule = new BusinessSchedule()
    .Monday("09:00", "17:00")
    .Tuesday("09:00", "17:00")
    .Wednesday("09:00", "17:00")
    .Thursday("09:00", "17:00")
    .Friday("09:00", "17:00")
    .Saturday()   // closed
    .Sunday()     // closed
    .WithTimezone("America/New_York")
    .WithHolidays(HolidayCalendar.ForCountry("US", 2026));

// 2. Instantiate the calculator
var calculator = new BusinessScheduleCalculator();

// 3. Add 4 business hours to a ticket creation time
var ticketCreated = new DateTimeOffset(2026, 11, 25, 14, 0, 0, TimeSpan.Zero); // Wed 14:00 UTC
var deadline = calculator.AddBusinessTime(ticketCreated, TimeSpan.FromHours(4), schedule);
// Wed has 1h left (14:00–17:00 ET = 19:00–22:00 UTC? — no, schedule timezone is ET)
// Result: Thu Nov 26 is Thanksgiving (skipped), so spills to Fri Nov 27

// 4. Check if a moment is within business hours
bool isOpen = calculator.IsWithinBusinessHours(DateTimeOffset.UtcNow, schedule);

// 5. Calculate elapsed business time between two points
var elapsed = calculator.ElapsedBusinessTime(ticketCreated, deadline, schedule);

Building a Schedule

Weekday Hours

Use the fluent weekday methods. Each method accepts "HH:mm" 24-hour open/close strings. Calling the parameterless overload marks the day as closed.

var schedule = new BusinessSchedule()
    .Monday("09:00", "17:00")
    .Tuesday("09:00", "17:00")
    .Wednesday("09:00", "17:00")
    .Thursday("09:00", "17:00")
    .Friday("09:00", "14:00")   // half day
    .Saturday()                 // closed
    .Sunday();                  // closed

All methods return a new BusinessSchedule — the original is never mutated, so you can safely derive multiple variants from a base schedule:

var base = new BusinessSchedule()
    .Monday("09:00", "17:00")
    .Friday("09:00", "17:00");

var withLunchBreak  = base.Wednesday("09:00", "12:00"); // only Wednesday differs
var withFullWednes  = base.Wednesday("09:00", "17:00");

Timezone Support

// IANA timezone IDs (cross-platform)
var schedule = new BusinessSchedule()
    .Monday("09:00", "17:00")
    .WithTimezone("America/New_York");

// Windows timezone IDs (also accepted)
var schedule2 = new BusinessSchedule()
    .Monday("09:00", "17:00")
    .WithTimezone("Eastern Standard Time");

All calculations convert the DateTimeOffset inputs to the schedule's timezone before applying business-hours logic. Results are always returned in UTC.

Holidays

Holidays are treated as fully closed days regardless of the day-of-week configuration.

Built-in US Holidays

using ScheduleKit.Holidays;

// Get all 11 US federal holidays for 2026
IReadOnlyList<Holiday> us2026 = HolidayCalendar.ForCountry("US", 2026);

var schedule = new BusinessSchedule()
    .Monday("09:00", "17:00")
    // ... other days
    .WithHolidays(us2026);

Supported countries: "US" (case-insensitive).

US holidays included (with standard Saturday→Friday / Sunday→Monday observation):

  • New Year's Day (Jan 1)
  • Martin Luther King Jr. Day (3rd Monday in January)
  • Presidents' Day (3rd Monday in February)
  • Memorial Day (last Monday in May)
  • Juneteenth National Independence Day (Jun 19)
  • Independence Day (Jul 4)
  • Labor Day (1st Monday in September)
  • Columbus Day (2nd Monday in October)
  • Veterans Day (Nov 11)
  • Thanksgiving Day (4th Thursday in November)
  • Christmas Day (Dec 25)

Custom Holidays

// Add a single date
var schedule = new BusinessSchedule()
    .Monday("09:00", "17:00")
    .WithCustomHoliday(new DateOnly(2026, 12, 26), "Boxing Day");

// Or supply a DateTime
var schedule2 = schedule.WithCustomHoliday(new DateTime(2026, 4, 10), "Good Friday");

// Or compose from any IEnumerable<Holiday>
var companyHolidays = new[]
{
    new Holiday(new DateOnly(2026, 7, 3),  "Company Summer Day"),
    new Holiday(new DateOnly(2026, 12, 24), "Christmas Eve"),
};
var schedule3 = new BusinessSchedule()
    .Monday("09:00", "17:00")
    .WithHolidays(companyHolidays);

Calculations

All calculations require a BusinessSchedule and operate in the schedule's configured timezone.

AddBusinessTime

Add a TimeSpan of business time to a starting point, skipping weekends and holidays.

var calculator = new BusinessScheduleCalculator();

// Friday 15:00 UTC + 4 business hours
// (2 hours remain Friday, then skip weekend, continue Monday morning)
var from   = new DateTimeOffset(2026, 3, 27, 15, 0, 0, TimeSpan.Zero);
var result = calculator.AddBusinessTime(from, TimeSpan.FromHours(4), schedule);
// => Monday 2026-03-30 11:00 UTC

Algorithm:

  1. Convert from to the schedule's timezone.
  2. If outside business hours, advance to the next opening time.
  3. While remaining > 0:
    • If remaining fits within today's remaining business hours → advance by that amount → done.
    • Otherwise → subtract today's remaining hours, advance to the next business day's opening.
  4. Return result in UTC.

ElapsedBusinessTime

Calculate how much business time has passed between two points.

var from    = new DateTimeOffset(2026, 3, 27, 14, 0, 0, TimeSpan.Zero); // Friday 14:00
var to      = new DateTimeOffset(2026, 3, 30, 12, 0, 0, TimeSpan.Zero); // Monday 12:00
var elapsed = calculator.ElapsedBusinessTime(from, to, schedule);
// => 6 hours  (Fri 14–17 = 3h, Mon 09–12 = 3h, weekend excluded)

Algorithm:

Walk day-by-day from from to to, summing only the portions of each day that overlap with the day's business hours window. Days that are closed (weekends, holidays) contribute zero.

IsWithinBusinessHours

Check whether a specific moment falls within business hours.

bool isOpen = calculator.IsWithinBusinessHours(
    DateTimeOffset.UtcNow, schedule);

The close time is exclusive — a moment exactly at the closing time returns false.

NextOpeningTime

Find the next moment the business will be open. Returns the input moment itself if it is already within business hours.

// Saturday noon → returns Monday 09:00
var next = calculator.NextOpeningTime(
    new DateTimeOffset(2026, 3, 28, 12, 0, 0, TimeSpan.Zero), schedule);

Dependency Injection

Default Schedule

Register ScheduleKit with a hard-coded schedule:

services.AddScheduleKit(options =>
{
    options.DefaultSchedule = new BusinessSchedule()
        .Monday("09:00", "17:00")
        .Tuesday("09:00", "17:00")
        .Wednesday("09:00", "17:00")
        .Thursday("09:00", "17:00")
        .Friday("09:00", "17:00")
        .Saturday()
        .Sunday()
        .WithTimezone("America/New_York")
        .WithHolidays(HolidayCalendar.ForCountry("US", DateTimeOffset.UtcNow.Year));
});

Custom Schedule Provider

Implement IScheduleProvider to load schedules from any source (database, per-tenant config, etc.):

public class TenantScheduleProvider(AppDbContext db) : IScheduleProvider
{
    public async Task<BusinessSchedule> GetScheduleAsync(
        object? scopeId = null,
        CancellationToken ct = default)
    {
        var tenantId = scopeId as int? ?? throw new InvalidOperationException("scopeId must be a tenant ID.");
        var row = await db.TenantSchedules.FindAsync([tenantId], ct)
            ?? throw new InvalidOperationException($"No schedule found for tenant {tenantId}.");

        return new BusinessSchedule()
            .Monday(row.MondayOpen, row.MondayClose)
            .WithTimezone(row.TimezoneId);
    }
}

Register it:

services.AddScheduleKit(options =>
{
    options.ResolveScheduleFrom<TenantScheduleProvider>();
});

IScheduleCalculator

Inject IScheduleCalculator to resolve the schedule automatically from the provider:

public class SlaService(IScheduleCalculator calculator)
{
    public async Task<DateTimeOffset> GetDeadlineAsync(
        DateTimeOffset ticketCreated,
        TimeSpan sla,
        int tenantId,
        CancellationToken ct = default)
    {
        return await calculator.AddBusinessTimeAsync(ticketCreated, sla, tenantId, ct);
    }
}

IScheduleCalculator exposes:

Method Description
AddBusinessTimeAsync Add business duration to a start point
ElapsedBusinessTimeAsync Compute elapsed business time
IsWithinBusinessHoursAsync Check if a moment is open
NextOpeningTimeAsync Find next opening moment

ASP.NET Core Integration

Install the integration package:

dotnet add package ScheduleKit.AspNetCore

Register a scoped provider that can take a dependency on IHttpContextAccessor or other scoped services:

using ScheduleKit.AspNetCore.DependencyInjection;

services.AddScheduleKitWithProvider<MyHttpContextAwareProvider>(options =>
{
    options.DefaultSchedule = new BusinessSchedule()
        .Monday("09:00", "17:00");
});

Algorithm Details

AddBusinessTime

Given from and duration:

  1. Convert from to the schedule timezone.
  2. If the current moment is outside business hours (before open, after close, closed day, holiday) → advance to the next opening time.
  3. Loop:
    1. Compute time remaining until close on the current day.
    2. If remaining <= timeLeftToday → move forward by remaining → done.
    3. Otherwise → subtract timeLeftToday from remaining → advance to the opening of the next business day.
  4. Return result converted to UTC.

The loop is guarded with a 3650-iteration limit (10 years of daily iterations). If no open day is found within that limit, an InvalidOperationException is thrown — this protects against schedules with no open days.

ElapsedBusinessTime

Given from and to:

  1. Walk day by day from from to to.
  2. For each calendar day:
    • If it is a non-business day (weekend or holiday) → skip.
    • Otherwise, clamp the time range to [openTime, closeTime] for that day and add the overlap to the running total.
  3. Return total elapsed business time as a TimeSpan.

Multi-targeting

Package Target Frameworks
ScheduleKit net8.0, net9.0, net10.0
ScheduleKit.AspNetCore net8.0, net10.0

License

MIT — see LICENSE.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on ScheduleKit:

Package Downloads
ScheduleKit.AspNetCore

ASP.NET Core integration for ScheduleKit — dependency injection extensions and schedule provider registration.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 168 3/26/2026