{"id":32958244,"url":"https://github.com/Occurify/Occurify","last_synced_at":"2025-11-16T16:02:01.544Z","repository":{"id":282019564,"uuid":"947212739","full_name":"Occurify/Occurify","owner":"Occurify","description":"A powerful and intuitive .NET library for defining, filtering, transforming, and scheduling instant and period timelines.","archived":false,"fork":false,"pushed_at":"2025-09-26T12:19:02.000Z","size":499,"stargazers_count":68,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-09-26T12:25:14.952Z","etag":null,"topics":["astronomy","coordinates","cron","cronjob","datetime","fluent-design","instant","period","periods","reactivex","recurring","recursion","scheduling","sun-position","sunrise-sunset-api","time","time-period","timeline","timelines","timezone"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Occurify.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-03-12T10:30:14.000Z","updated_at":"2025-09-26T12:18:41.000Z","dependencies_parsed_at":"2025-04-04T11:21:24.512Z","dependency_job_id":"d81531a3-6ecc-4bb2-b138-db2a9ca6ef74","html_url":"https://github.com/Occurify/Occurify","commit_stats":null,"previous_names":["occurify/occurify"],"tags_count":15,"template":false,"template_full_name":null,"purl":"pkg:github/Occurify/Occurify","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Occurify%2FOccurify","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Occurify%2FOccurify/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Occurify%2FOccurify/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Occurify%2FOccurify/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Occurify","download_url":"https://codeload.github.com/Occurify/Occurify/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Occurify%2FOccurify/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":284734138,"owners_count":27054622,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-11-16T02:00:05.974Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["astronomy","coordinates","cron","cronjob","datetime","fluent-design","instant","period","periods","reactivex","recurring","recursion","scheduling","sun-position","sunrise-sunset-api","time","time-period","timeline","timelines","timezone"],"created_at":"2025-11-12T23:00:34.016Z","updated_at":"2025-11-16T16:02:01.536Z","avatar_url":"https://github.com/Occurify.png","language":"C#","readme":"# Occurify\n\n[![GitHub license](https://img.shields.io/github/license/Occurify/Occurify?label=License)](https://github.com/Occurify/Occurify?tab=MIT-1-ov-file)\n[![GitHub release](https://img.shields.io/github/v/release/Occurify/Occurify?label=Release)](https://github.com/Occurify/Occurify/releases/latest)\n[![Build Status](https://github.com/Occurify/Occurify/actions/workflows/ci-build-and-test.yml/badge.svg)](https://github.com/Occurify/Occurify/actions/workflows/ci-build-and-test.yml)\n\nA powerful and intuitive .NET library for defining, filtering, transforming, and scheduling instant and period timelines.\n\n## 📖 Table of Contents  \n- [Overview](#overview)\n- [Installation](#installation)\n- [Usage](#usage)\n    - [Defining Timelines](#defining-timelines)\n    - [Transforming Timelines](#transforming-timelines)\n    - [Combining Timelines Into Periods](#combining-timelines-into-periods)\n    - [Filtering \u0026 Randomization](#filtering--randomization)\n    - [Sampling](#checking-if-the-lights-should-be-on-right-now)\n    - [Enumerating](#enumerating-future-or-past-events)\n    - [Scheduling](#scheduling-automatic-actions)\n- [Potential Use Cases](#potential-use-cases)\n    - [Morning Light](#morning-light)\n    - [Use Crons to Create Periods](#use-crons-to-create-periods)\n    - [Working with Different Periods](#working-with-different-periods)\n    - [Searching Dates](#searching-dates)\n    - [Finding Available Periods Between Reservations](#finding-available-periods-between-reservations)\n    - [Solar Phases](#solar-phases)\n    - [Complicated Requirements](#complicated-requirements)\n    - [Working with Multiple Periods](#working-with-multiple-periods)\n- [Design](#design)\n    - [Instant](#instant)\n    - [Period](#period)\n    - [Instant Timeline](#instant-timeline)\n    - [Period Timeline](#period-timeline)\n    - [Collections and Dictionaries](#collections-and-dictionaries)\n- [Coordinates](#coordinates)\n- [ASCII Representation of Timelines](#ascii-representation-of-timelines)\n- [Extension Methods](#extension-methods)\n- [Unit Tests](#unit-tests)\n- [Important Considerations](#important-considerations)\n- [Versioning and Stability](#versioning-and-stability)\n- [Release Notes](#release-notes)\n- [Multi-Language Support](#multi-language-support)\n- [License](#license)\n\n## Overview\n\n### [Occurify](https://www.nuget.org/packages/Occurify)\n\nA powerful and intuitive .NET library for defining, filtering, transforming, and scheduling instant and period timelines.\n\n- Supports instants and periods.\n- Supports instant and period timelines.\n- Supports (period)timeline collections and dictionaries.\n- Supports an extensive set of fluent extension methods to filter and transform instants, periods, timelines and period timelines.\n- Includes 4500+ unit tests to ensure reliability.\n\n### [Occurify.TimeZones](https://www.nuget.org/packages/Occurify.TimeZones)\n\nTime zone and cron expression support for Occurify: Filter, manipulate, and schedule instants and periods across time zones.\n\n- Supports time zone based instants and periods (e.g. time of day, day, week, etc).\n- Supports both forwards and backwards iteration through Cron instants and periods.\n- Uses the *Cronos* library to enable Cron functionality that:\n    - Supports standard Cron format with optional seconds.\n    - Supports time zones, and performs all the date/time conversions for you.\n    - Does not skip occurrences, when the clock jumps forward to daylight saving time (known as Summer time).\n    - Does not skip interval-based occurrences, when the clock jumps backward from Summer time.\n    - Does not retry non-interval based occurrences, when the clock jumps backward from Summer time.\n- Supports time zone `ToString` method implementations for both `Period` and `DateTime`\n\n### [Occurify.Astro](https://www.nuget.org/packages/Occurify.Astro)\n\nAstronomical instants and periods for Occurify: Track sun states, perform calculations, and manage events.\n\n- Uses the *SunCalcNet* library to enable functionality that:\n- Supports location (coordinate) based instants and periods (e.g. dawn, daytime, etc).\n- Supports multiple solar phases (sunrise, sunset, end of sunrise, start of sunset, (nautical) dawn, (nautical) dusk, (end of) night, (end of) golden hour, solar noon and nadir).\n\n### [Occurify.Reactive](https://www.nuget.org/packages/Occurify.Reactive)\n\nReactive Extensions for Occurify: Enabling seamless scheduling of instant and period-based timelines.\n\n- Uses ReactiveX to enable scheduling for both timelines and periods.\n\n## Installation\n\nOccurify is distributed as the following NuGet packages:\n\nPackage | Description\n--- |---\n[Occurify](https://www.nuget.org/packages/Occurify) | A powerful and intuitive .NET library for defining, filtering, transforming, and scheduling instant and period timelines.\n[Occurify.TimeZones](https://www.nuget.org/packages/Occurify.TimeZones) | Time zone and cron expression support for Occurify: Filter, manipulate, and schedule instants and periods across time zones.\n[Occurify.Astro](https://www.nuget.org/packages/Occurify.Astro) | Astronomical instants and periods for Occurify: Track sun states, perform calculations, and manage events.\n[Occurify.Reactive](https://www.nuget.org/packages/Occurify.Reactive) | Reactive Extensions for Occurify: Enabling seamless scheduling of instant and period-based timelines.\n\nTo install the core Occurify package, use the NuGet Package Manager Console:\n\n```powershell\nPM\u003e Install-Package Occurify\n```\n\nAlternatively, you can install it using the .NET CLI:\n\n```powershell\ndotnet add package Occurify\n```\n\nFor other packages, replace `Occurify` with the desired package name.\n\n## Usage\n\nInstead 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.\n\nAs an example, let’s imagine we want to automate our lights to turn on in the evening.\n\n### Defining Timelines\n\nInstead of manually maintaining a list of sunset times, we can simply use:\n\n```cs\nITimeline sunsets = AstroInstants.LocalSunsets;\n```\n\nThis timeline now represents every sunset dynamically—no need for hardcoded schedules.\n\n### Transforming Timelines\n\nWant to schedule events **20 minutes after sunset**? Just shift the timeline:\n\n```cs\nITimeline twentyMinAfterSunset = sunsets + TimeSpan.FromMinutes(20);\n```\n\nNow, `twentyMinAfterSunset` dynamically represents every sunset, plus 20 minutes—no manual calculations needed.\n\n### Combining Timelines Into Periods\n\nNow, let’s define a time when the lights should **turn off** and create a period from **20 minutes after sunset** until **11 PM**:\n\n```cs\nITimeline elevenPm = TimeZoneInstants.DailyAt(hour: 23);\nIPeriodTimeline lightOnPeriods = twentyMinAfterSunset.To(elevenPm);\n```\n\nWith this, lightOnPeriods now represents **all the evening periods** when the lights should be on.\n\n### Filtering \u0026 Randomization\n\nIf you want the lights to turn on **only on weekdays**, you can filter the periods like this:\n\n```cs\nlightOnPeriods = lightOnPeriods.Within(TimeZonePeriods.Workdays());\n```\n\nTo make the timing feel more natural, we can **randomize** the periods slightly by adding a variation of up to 10 minutes:\n\n```cs\nlightOnPeriods = lightOnPeriods.Randomize(TimeSpan.FromMinutes(10));\n```\n\n### Using the Timeline\n\n#### Checking if the Lights Should Be On Right Now\n\nTo check if the lights should be on at the current moment, you can simply use `IsNow()` on the `lightOnPeriods` timeline:\n\n```cs\nif (lightOnPeriods.IsNow()) {\n    // Turn lights on.\n}\nelse {\n    // Turn lights off.\n}\n```\n\n#### Enumerating Future (or Past) Events\n\nYou 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**:\n\n```cs\nConsole.WriteLine(\"The rest of the current month the lights will go on at:\");\nforeach (Period period in lightOnPeriods.EnumerateRange(DateTime.UtcNow, TimeZonePeriods.CurrentMonth().End!.Value)){\n    Console.WriteLine(period.Start!.Value.ToLocalTime());\n}\n```\n\nBut due to the dynamic nature of timelines we can just as easily see when the lights will turn on in **February 2050**:\n\n```cs\nConsole.WriteLine(\"In February 2050 the lights will go on at:\");\nforeach (Period period in lightOnPeriods.EnumeratePeriod(TimeZonePeriods.Month(2, 2050))){\n    Console.WriteLine(period.Start!.Value.ToLocalTime());\n}\n```\n\u003e Note that the period timeline **only resolves the necessary periods when enumerated**, ensuring efficiency.\n\n#### Scheduling Automatic Actions\n\nTo 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.\n\nBy default, this method even evaluates the current state of the timeline, invoking the applicable method on startup.\n\n```cs\nlightOnPeriods.SubscribeStartEnd(() =\u003e TurnLightsOn(), () =\u003e TurnLightsOff(), scheduler);\n```\n\nThis 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**.\n\n## Potential Use Cases\n\nThis section presents various use cases that demonstrate Occurify’s capabilities and provide a clearer understanding of its functionality.\n\nThese examples incorporate additional modules such as `Occurify.TimeZones`, `Occurify.Astro`, and `Occurify.Reactive`.\n\n\u003e**Note: Instead of using `var`, variable types are explicitly defined in the examples for improved clarity.**\n\n### Morning Light\n\nThe following example demonstrates how to turn on a light between **7 AM and 15 minutes after sunrise**.\n\n\u003e **Note:** `AstroInstants` is provided by `Occurify.Astro`, while `TimeZoneInstants` comes from `Occurify.TimeZones`.\n\n#### Defining the Period\n```cs\nITimeline fifteenMinAfterSunRise = AstroInstants.LocalSunrises + TimeSpan.FromMinutes(15);\nITimeline sevenAm = TimeZoneInstants.DailyAt(hour: 7);\nIPeriodTimeline between7AndSunRise = sevenAm.To(fifteenMinAfterSunRise); // Creates timeline that represents periods starting at 7am and ending 15 minutes after sunrise.\n```\n\n#### Handling the Edge Case: Sunrise Before 7 AM\nA potential issue arises: what if sunrise occurs **before** 7 AM? In this case, our period would incorrectly extend into the previous day.\n\nTo ensure our period stays within the intended day, we can apply a filter:\n```cs\nIPeriodTimeline between7AndSunRiseInTheMorning = between7AndSunRise.Within(TimeZonePeriods.Days());\n```\n\u003e Alternatively, we could have used the helper method `TimeZonePeriods.DailyBetween`\n\n#### Using ReactiveX for Scheduling\n\nNow we can use `SubscribeStartEnd` from `Occurify.Reactive` to integrate with `ReactiveX` for event-driven scheduling:\n```cs\nbetween7AndSunRiseInTheMorning.SubscribeStartEnd(() =\u003e lightEntity.TurnOn(), () =\u003e lightEntity.TurnOff(), scheduler);\n```\n\nThis ensures the light automatically turns **on or off** based on whether the current time falls within the defined period. Additionally, it will set the light to the correct state initially, matching the period's condition at the time the program starts.\n\n### Use Crons to Create Periods\n\nThe following example demonstrates the use of cron expressions using `Occurify.TimeZones`. Not only does it allow you to create a timeline with instants, but it can also convert a cron expression directly into a period-based timeline (e.g., hours, days).\n\nThis example calculates how many working days there are if we exclude public holidays.\n```cs\nstring[] holidayCrons = [\n    \"0 0 0 1 1 ?\", // New Year Day\n    \"0 0 0 ? 5 MON#4\", //Memorial Day\n    \"0 0 0 4 7 ?\", //Independence Day\n    \"0 0 0 ? 9 MON#1\", //Labor Day\n    \"0 0 0 ? 11 THU#4\", //Thanksgiving\n    \"0 0 0 25 12 ?\" //Christmas\n];\n\nIPeriodTimeline holidays = TimeZonePeriods.Days(holidayCrons.Select(TimeZoneInstants.FromCron).Combine());\nIPeriodTimeline workingDays = TimeZonePeriods.Workdays();\nIPeriodTimeline workingDaysWithoutHolidays = workingDays - holidays;\n\nConsole.WriteLine(\"Work days this year:\");\nforeach (Period period in workingDaysWithoutHolidays.EnumeratePeriod(TimeZonePeriods.CurrentYear()))\n{\n    Console.WriteLine(period.Start!.Value.ToLocalTimeZoneShortDateString());\n}\n```\n\n### Working with Different Periods\n\nOccurify allows us to define periods however we want. In this example we use `TimeZoneInstants.StartOfMonth(10)` to get a timeline with the start of every October. Using `AsConsecutivePeriodTimeline` turns those instants into consecutive periods that we can use to represent fiscal years.\n\n```cs\nIPeriodTimeline calendarYears = TimeZonePeriods.Years();\nIPeriodTimeline fiscalYears = TimeZoneInstants.StartOfMonths(10).AsConsecutivePeriodTimeline();\n\nPeriod currentCalendarYear = calendarYears.SampleAt(DateTime.UtcNow).Period!;\nPeriod currentFiscalYear = fiscalYears.SampleAt(DateTime.UtcNow).Period!;\n```\n\nNext, we can use `EnumerateRange` to count how many workdays have passed in both the calendar and fiscal years:\n\n```cs\nIPeriodTimeline workdays = TimeZonePeriods.Workdays();\nint amountOfCalendarDaysWorked = workdays.EnumerateRange(currentCalendarYear.Start!.Value, DateTime.UtcNow).Count();\nint amountOfFiscalYearDaysWorked = workdays.EnumerateRange(currentFiscalYear.Start!.Value, DateTime.UtcNow).Count();\n```\n\n### Searching Dates\n\nHere's an example of how you can find out how many Fridays there were in February of the years 1200 and 1201:\n\n```cs\nIPeriodTimeline fridaysOfFebruary = TimeZonePeriods.Months(2, TimeZoneInfo.Utc) \u0026 TimeZonePeriods.Days(DayOfWeek.Friday, TimeZoneInfo.Utc);\nPeriod twoYears = TimeZoneInstants.StartOfYear(1200, TimeZoneInfo.Utc).To(TimeZoneInstants.EndOfYear(1201, TimeZoneInfo.Utc));\n\nConsole.WriteLine(\"The years 1200 and 1201 have the following fridays in february:\");\nforeach (var date in fridaysOfFebruary.EnumeratePeriod(twoYears))\n{\n    Console.WriteLine(date.Start!.Value.ToLocalTimeZoneShortDateString());\n}\n```\n\n### Finding Available Periods Between Reservations\n\nThis example demonstrates how to efficiently identify gaps of a minimum duration within a set of reservations, constrained to a specific search range.\n```cs\npublic Period[] FindAvailableFreePeriods(Period searchRange, Period[] reservations, TimeSpan minimumDuration)\n{\n    return reservations\n        .Invert() // Identify free periods by inverting the reserved ones\n        .WherePeriods(p =\u003e p.Duration \u003e= minimumDuration) // Filter periods that meet the minimum duration requirement\n        .EnumeratePeriod(searchRange) // Restrict results to the given search range\n        .ToArray();\n}\n```\n\n### Solar Phases\n\nIn this example, we calculate how many days in the current year in the Arctic region don't experience either a sunset or a sunrise:\n\n```cs\nCoordinates arcticCoordinates = new Coordinates(80.45302, 54.77918, Height: 37);\nITimeline sunsetsAndRises = AstroInstants.SunPhases(arcticCoordinates, SunPhases.Sunrise | SunPhases.Sunset);\nIPeriodTimeline daysOfCurrentYear = TimeZonePeriods.Days().Within(TimeZonePeriods.CurrentYear());\nIPeriodTimeline daysWithoutSunsetsOrRises = daysOfCurrentYear - daysOfCurrentYear.Containing(sunsetsAndRises);\n\nConsole.WriteLine($\"This year on the arctic the sun doesn't rise or set on {daysWithoutSunsetsOrRises.Count()} days.\");\n```\n\n### Complicated Requirements\n\nSome scenarios require more complex scheduling logic. In this example, we want to turn on the living room lights in the evening when we're on vacation, but with the following conditions:\n\n- Turn on **10 minutes after sunset**, with a **random deviation of 5 minutes before and 10 minutes after** to make it look natural.\n- **Never turn on before 5:15 PM** and **never after 8 PM**.\n- **Turn off at 11:45 PM**, with a **random deviation of ±20 minutes**.\n- Ensure consistent behavior across restarts by using a fixed random seed.\n\nHere's how we can achieve this using Occurify:\n\n```cs\nint seed = 1337;\n\n// Determine start instants\nITimeline tenMinAfterSunset = AstroInstants.LocalSunsets + TimeSpan.FromMinutes(10);\nITimeline randomizedSunset = tenMinAfterSunset.Randomize(seed, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(10));\n// Ensure the light doesn't turn on before 5:15 PM\nITimeline after515Pm = (randomizedSunset + TimeZoneInstants.DailyAt(hour: 17, minute: 15)).LastWithin(TimeZonePeriods.Days());\n// Ensure the light doesn't turn on after 8 PM\nITimeline turnOnAt = (after515Pm + TimeZoneInstants.DailyAt(hour: 20)).FirstWithin(TimeZonePeriods.Days());\n\n// Determine end instants\nITimeline turnOffAt = TimeZoneInstants.DailyAt(hour: 23, minute: 15).Randomize(seed, TimeSpan.FromMinutes(20));\n\n// Create period\nIPeriodTimeline lightOnPeriods = turnOnAt.To(turnOffAt);\n\n// Schedule\nlightOnPeriods.SubscribeStartEnd(() =\u003e lightEntity.TurnOn(), () =\u003e lightEntity.TurnOff(), scheduler);\n```\n\n#### Explanation\n\n- `Randomize(seed, minOffset, maxOffset)` ensures that randomness is applied consistently, even across restarts.\n- `LastWithin(TimeZonePeriods.Days())` makes sure we use the last valid time after the 5:15 PM cutoff.\n- `FirstWithin(TimeZonePeriods.Days())` finds the earliest valid moment within the defined constraints.\n- `SubscribeStartEnd(() =\u003e lightEntity.TurnOn(), () =\u003e lightEntity.TurnOff(), scheduler)` immediately updates the light to the correct state when the program starts and schedules future on/off transitions based on the defined period.\n\nThis logic ensures the light turns on and off naturally based on the specified conditions, making automation less predictable and more human-like.\n\n### Working with Multiple Periods\n\nOccurify also makes it easy to work with multiple schedules and availability periods. This example demonstrates how to determine when employees are free for a meeting and how many people were in appointments at a given time.\n\n#### Defining Working Hours and Availability\n\n```cs\nIPeriodTimeline workingHours = TimeZonePeriods.Between(startHour: 8, endHour: 18) - TimeZonePeriods.Weekends();\n\nList\u003cPeriod[]\u003e employeeAppointments = CustomLogic.LoadAppointments();\nIPeriodTimeline[] appointmentTimelines = employeeAppointments.Select(p =\u003e p.AsPeriodTimeline()).ToArray();\nIPeriodTimeline[] invertedTimelines = appointmentTimelines.Invert().ToArray();\n\nIPeriodTimeline availableSlotsTimelines = invertedTimelines.IntersectPeriods() \u0026 workingHours;\n```\n\n#### Finding Common Availability\n\nTo check which periods all employees are available for a meeting in August 2025:\n\n```cs\nPeriod august = TimeZonePeriods.Month(8, 2025);\nIEnumerable\u003cPeriod\u003e periodsEveryoneIsAvailable = availableSlotsTimelines\n    .EnumeratePeriod(august).Where(p =\u003e p.Duration \u003e TimeSpan.FromHours(1));\n\nConsole.WriteLine(\"Everyone is available on:\");\nforeach (Period period in periodsEveryoneIsAvailable)\n{\n    Console.WriteLine(period.ToLocalTimeZoneString());\n}\n```\n\n#### Checking How Many People Had an Appointment\n\nTo see how many employees had a meeting at a specific time:\n\n```cs\nDateTime timeOfInterest = new DateTime(2025, 3, 7).AsUtcInstant();\nPeriodTimelineSample[] samples = appointmentTimelines.SampleAt(timeOfInterest).ToArray();\n\nint appointmentPeriods = samples.Count(s =\u003e s.IsPeriod);\nint freeTimePeriods = samples.Count(s =\u003e s.IsGap);\n\nConsole.WriteLine($\"{appointmentPeriods} people had an appointment on {timeOfInterest.ToLocalTimeZoneString()}.\");\nConsole.WriteLine($\"{freeTimePeriods} people had were free on {timeOfInterest.ToLocalTimeZoneString()}.\");\n```\n\n## Design\n\nOccurify uses 4 main concepts:\n\nConcept | Represented by | Description\n--- | --- | ----\n[Instant](#instant) | UTC `DateTime` | A single instant in time.\n[Period](#period) | `Period` | A period of time, defined by a start and end instant.\n[Instant timeline](#instant-timeline) | `ITimeline` | A timeline containing instants.\n[Period timeline](#period-timeline) | `IPeriodTimeline` | A timeline containing periods.\n\n\u003e Note that in Occurify, collections of `ITimeline` and `IPeriodTimeline` are also treated as first-class citizens. More on that [here](#collections-and-dictionaries).\n\n### Instant\n\nAn instant is represented using a `DateTime` with `Kind` set to `DateTimeKind.Utc`.\n\nThe valid range for an instant is from `01-01-0000` to `31-12-9999`.\n\n### Period\n\nA period is defined by two instants: a start and an end.\n\n- If the start is null, it means the period has no defined beginning (i.e., it started at the beginning of time).\n- If the end is null, it means the period has no defined end (i.e., it lasts indefinitely).\n- If both start and end are null, the period is infinite in both directions.\n\nA period contains all instants that are greater than or equal to the start instant and smaller than the end instant:\n`(Start == null || instant \u003e= Start) \u0026\u0026\n(End == null || instant \u003c End)`\n\n**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.\n\n#### Period Record:\n```cs\nrecord Period(DateTime? Start, DateTime? End) : IComparable\u003cPeriod\u003e\n```\n\n#### Different ways of creating a period:\n```cs\nDateTime utcNow = DateTime.UtcNow;\n\n// Using extension methods\nPeriod nowToOneHoursFromNow = utcNow.ToPeriodWithDuration(TimeSpan.FromHours(1));\nPeriod nowToTwoHoursFromNow = utcNow.To(utcNow + TimeSpan.FromHours(2));\nPeriod nowToNeverEnding = utcNow.To(null);\n\n// Using static methods\nPeriod nowToThreeHoursFromNow = Period.Create(utcNow, utcNow + TimeSpan.FromHours(3));\n```\n\n### Instant Timeline\n\nAn instant timeline represents a timeline of instants.\n\nAlthough an instant timeline implements `IEnumerable\u003cDateTime\u003e`, 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.\n\nWithin Occurify, a timeline has the following properties:\n- **Immutable:** Once created, an instant timeline cannot be modified.\n- **Deterministic:** The timeline will always yield the same instants given the same parameters.\n- **Works with UTC:** All instants in an instant timeline are represented as `DateTime` values in UTC (`DateTimeKind.Utc`).\n\nAn instant timeline implements `IEnumerable\u003cDateTime\u003e`, meaning that you can enumerate through it to access all instants, starting from the earliest to the latest.\n\n#### Timeline Interface:\n```cs\npublic interface ITimeline : IEnumerable\u003cDateTime\u003e\n{\n    DateTime? GetPreviousUtcInstant(DateTime utcRelativeTo);\n    DateTime? GetNextUtcInstant(DateTime utcRelativeTo);\n    bool IsInstant(DateTime utcDateTime);\n}\n```\n\n#### Different ways of creating an instant pipeline:\n```cs\nDateTime utcNow = DateTime.UtcNow;\n\n// Using extension methods\nITimeline timeline1 = utcNow.AsTimeline();\nITimeline timeline2 = new[] { utcNow, utcNow + TimeSpan.FromHours(1) }.AsTimeline();\n\n// Using static methods\nITimeline timeline3 = Timeline.FromInstants(utcNow, utcNow + TimeSpan.FromHours(1), utcNow, utcNow + TimeSpan.FromHours(3));\nITimeline timeline4 = Timeline.Periodic(TimeSpan.FromHours(1));\n```\nNote 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.\n\n### Period Timeline\n\nA period timeline is defined by a start timeline and an end timeline and represents a timeline of periods.\n\nPeriods 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.\n\nDue 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\".\n\nIf 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.\n\nIf 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.\n\nA period timeline also implements `IEnumerable\u003cPeriod\u003e`. Enumerating a timeline will iterate all instants in a timeline from earliest to latest.\n\n#### Period Timeline Interface:\n```cs\npublic interface IPeriodTimeline : IEnumerable\u003cPeriod\u003e\n{\n    ITimeline StartTimeline { get; }\n    ITimeline EndTimeline { get; }\n}\n```\n\n#### Different ways of creating an period pipeline:\n```cs\nDateTime utcNow = DateTime.UtcNow;\nPeriod period = utcNow.To(utcNow + TimeSpan.FromHours(2));\n\n// Using extension methods on periods\nIPeriodTimeline periodTimeline1 = period.AsPeriodTimeline();\nIPeriodTimeline periodTimeline2 = new[] { period, period + TimeSpan.FromHours(2) }.AsPeriodTimeline();\n\n// Using extension methods on instant timelines\nITimeline periodStartTimeline = Timeline.Periodic(TimeSpan.FromHours(1));\nITimeline periodEndTimeline = periodStartTimeline.OffsetMinutes(10);\nIPeriodTimeline periodTimeline3 = periodStartTimeline.To(periodEndTimeline);\n\n// Using static methods\nIPeriodTimeline periodTimeline4 = PeriodTimeline.FromPeriods(period, period + TimeSpan.FromHours(2), period + TimeSpan.FromHours(4));\nIPeriodTimeline periodTimeline5 = PeriodTimeline.Between(periodStartTimeline, periodEndTimeline);\n```\n\n### Collections and Dictionaries\n\nIn Occurify, collections of `ITimeline` and `IPeriodTimeline` are treated as first-class citizens. This means you can use all extension methods for `ITimeline` and `IPeriodTimeline` not only on single instances, but also on the following collection types:\n- `IEnumerable\u003cITimeline\u003e`\n- `IEnumerable\u003cKeyValuePair\u003cTKey, ITimeline\u003e\u003e`\n- `IEnumerable\u003cKeyValuePair\u003cITimeline, TValue\u003e\u003e`\n- `IEnumerable\u003cIPeriodTimeline\u003e`\n- `IEnumerable\u003cKeyValuePair\u003cTKey, IPeriodTimeline\u003e\u003e`\n- `IEnumerable\u003cKeyValuePair\u003cIPeriodTimeline, TValue\u003e\u003e`\n\nThis is particularly powerful when you want to associate additional state or metadata (such as booleans, labels, or categories) with each timeline while still applying timeline operations across the collection.\n\nExample for `ITimeline`:\n\n```cs\nDictionary\u003cITimeline, bool\u003e sunStates = new Dictionary\u003cITimeline, bool\u003e\n{\n    { AstroInstants.LocalSunrises, true },\n    { AstroInstants.LocalSunsets, false }\n};\nforeach (KeyValuePair\u003cDateTime, bool[]\u003e state in sunStates.EnumeratePeriod(TimeZonePeriods.CurrentMonth()))\n{\n    bool sunIsRising = state.Value.First(); // Since we're combining multiple timelines, a single instant may correspond to multiple values. However, for this example, we assume sunrise and sunset don't occur simultaneously, so we just take the first value.\n    Console.WriteLine(sunIsRising ?\n        $\"At {state.Key.ToLocalTime()} the sun is rising!\" :\n        $\"At {state.Key.ToLocalTime()} the sun is setting!\");\n}\n```\n\n## Coordinates\n\nAll methods in `Occurify.Astro` have a signature with and without `Coordinates` object. If no `Coordinates` object is provided, the method will use `Coordinates.Local` by default. Note that this static property needs to be set before use:\n\n```cs\nCoordinates.Local = new Coordinates(78.2384, 15.4463, height: 126);\n```\n\n## ASCII Representation of Timelines\n\nFor both documentation (examples in this README) and testing purposes (discussed further in the [Unit Tests](#unit-tests) chapter), an ASCII notation is used to describe instants, periods, instant timelines and period timelines.\n\nThe following characters are used in the notation:\nCharacter | Meaning\n---|---\n*space* | No instant\n\\|| Instant on an instant timeline\n\u003c |Instant on start of a period timeline\n\\\u003e |Instant on end of a period timeline\nX|Instant on the exact same moment on both the start and end of a period timeline\n\nThis notation is used to describe timelines relative to each other. To ensure clarity, the ASCII lines should be properly aligned.\n\nThe following example shows how instants in a start and end timeline result in a period timeline:\n\n```\nStart instant timeline:    \"|  |    |  \"\nEnd instant timeline:      \"   |  |   |\"\nResulting period timeline: \"\u003c  X  \u003e \u003c \u003e\"\n```\n\n## Extension Methods\n\nOccurify provides a wide range of (fluent) extension methods for working with instants, periods, instant timelines, and period timelines. This chapter summarizes the most important methods and illustrates their functionality using ASCII notation. While not an exhaustive list, it highlights the methods that require the most explanation.\n\n### Enumeration\n\nBoth `ITimeline` and `IPeriodTimeline` derive from `IEnumerable\u003cDateTime\u003e` and `IEnumerable\u003cPeriod\u003e`. While iterating over these classes directly can be useful—especially when they contain a limited number of elements—we often need a subset of a timeline. To facilitate this, the following extension methods are implemented for both interfaces:\n\nMethod | Meaning\n--- |---\nEnumerate | Enumerates all instants/periods on the source timeline from earliest to latest.\nEnumerateBackwards | Enumerates all instants/periods on the source timeline from latest to earliest.\nEnumerateFrom | Enumerates all instants/periods on the source timeline that occur on or after a provided start instant from earliest to latest.\nEnumerateBackwardsTo | Enumerates all instants/periods on the source timeline that occur on or after a provided end instant from latest to earliest.\nEnumerateTo | Enumerates all instants/periods on the source timeline that occur earlier than a provided end instant from earliest to latest.\nEnumerateBackwardsFrom | Enumerates all instants/periods on the source timeline that occur earlier than a provided start instant from latest to earliest.\nEnumerateRange | Enumerates all instants/periods on the source timeline that occur between a provided start and end instant from earliest to latest.\nEnumerateRangeBackwards | Enumerates all instants/periods on the source timeline that occur between a provided start and end instant from latest to earliest.\nEnumeratePeriod | Enumerates all instants/periods on the source timeline that occur in a provided period from earliest to latest.\nEnumeratePeriodBackwards | Enumerates all instants/periods on the source timeline that occur in a provided period from latest to earliest.\n\nFor all extension methods on `IPeriodTimeline`, you can specify whether a period should be completely inside a range or if touching one or both sides of the range/period is also acceptable. For example, with `EnumerateTo`, there is an alternative method called `EnumerateToIncludingPartial`. Both the range and period methods accept an optional `PeriodIncludeOptions` parameter, which can be set to one of the following values: `CompleteOnly`, `StartPartialAllowed`, `EndPartialAllowed`, or `PartialAllowed`.\n\n### Filter ITimeline\n\n#### SkipWithin\n\nReturns a `ITimeline` in which the first x number instants within every period in a provided mask are bypassed.\n\n```\n\"source\": \"| | || | | \"\n\"mask  \": \"\u003c    X    \u003e\"\n\"skip  \": 1\n\"result\": \"  | |  | | \"\n```\n\n#### SkipLast\n\nReturns a `ITimeline` in which the last x number instants within every period in a provided mask are bypassed.\n\n```\n\"source\": \"| | || | | \"\n\"mask  \": \"\u003c    X    \u003e\"\n\"skip  \": 1\n\"result\": \"| |  | |   \"\n```\n\n#### Take\n\nReturns a `ITimeline` that contains the first x number instants within every period in a provided mask.\n\n```\n\"source\": \"| | || | | \"\n\"mask  \": \"\u003c    X    \u003e\"\n\"take  \": 1\n\"result\": \"|    |     \"\n```\n\n#### TakeLast\n\nReturns a `ITimeline` that contains the last x number instants within every period in a provided mask.\n\n```\n\"source\": \"| | || | | \"\n\"mask  \": \"\u003c    X    \u003e\"\n\"take  \": 1\n\"result\": \"    |    | \"\n```\n\n#### Containing\n\nFilters the source timeline based on which instants are also present in a provided instants/instant timeline.\n\n```\n\"source  \": \"| | | | | |\"\n\"instants\": \"  ||    || \"\n\"result  \": \"  |     |  \"\n```\n\n\u003e Containing is also implemented for the `\u0026` operator.\n\n#### Without\n\nFilters instants from a provided instants/instant timeline.\n\n```\n\"source  \": \"| | | | | |\"\n\"instants\": \"  ||    || \"\n\"result  \": \"|   | |   |\"\n```\n\n\u003e Without is also implemented for the `-` operator.\n\n#### Within\n\nFilters `ITimeline` based on which instants are inside any of the periods in a provided mask.\n\n```\n\"source\": \"| | || | | \"\n\"mask  \": \"\u003c   \u003e \u003c   \u003e\"\n\"result\": \"| |    | | \"\n```\n\n#### Outside\n\nFilters `ITimeline` based on which instants are not inside any of the periods in a provided mask.\n\n```\n\"source\": \"| | || | | \"\n\"mask  \": \"\u003c   \u003e \u003c   \u003e\"\n\"result\": \"    ||     \"\n```\n\n### Filter IPeriodTimeline\n\n#### Within\n\nFilters `IPeriodTimeline` based on which periods are inside any of the periods in a provided mask.\n\n```\n\"source\": \"\u003c \u003e  \u003c \u003e \u003c\u003e\"\n\"mask  \": \"\u003c   \u003e \u003c   \u003e\"\n\"result\": \"\u003c \u003e      \u003c\u003e\"\n```\n\n#### Outside\n\nFilters `IPeriodTimeline` based on which periods are not inside any of the periods in a provided mask.\n\n```\n\"source\": \"\u003c \u003e  \u003c \u003e \u003c\u003e\"\n\"mask  \": \"\u003c   \u003e \u003c   \u003e\"\n\"result\": \"     \u003c \u003e   \"\n```\n\n#### Containing\n\nFilters `IPeriodTimeline` based on which periods contain any of the provided periods.\n\n```\n\"source\": \"\u003c   \u003e \u003c   \u003e\"\n\"other \": \"\u003c \u003e  \u003c \u003e   \"\n\"result\": \"\u003c   \u003e      \"\n```\n\n#### Without\n\nFilters `IPeriodTimeline` based on which periods do not contain any of the periods.\n\n```\n\"source\": \"\u003c   \u003e \u003c   \u003e\"\n\"other \": \"\u003c \u003e  \u003c \u003e   \"\n\"result\": \"      \u003c   \u003e\"\n```\n\n### Transform ITimeline\n\n#### Combine\n\nReturns a `ITimeline` with the instants from both the source timeline and the provided instants/instant timeline.\n\n```\n\"source  \": \"| | | | | |\"\n\"instants\": \" |  |  |  | \"\n\"result  \": \"||| | ||| | \"\n```\n\n\u003e Combine is also implemented for the `+` and `|` operators.\n\n#### Offset\n\nOffsets the source timeline with a datetime.\n\n```\n\"source\": \"|   | |    \"\n\"offset\": 3\n\"result\": \"   |   | | \"\n```\n\n\u003e Offset is also implemented for the `+` and `-` operators in combination with a `TimeSpan`.\n\n#### Randomize\n\nRandomizes the source instants.\nThis method will never result in a change of instant count or in overlapping instants.\nIdentical inputs with the same seed, will result in the same output.\n\n#### To\n\nReturns a `IPeriodTimeline` with periods starting at instants on the source timeline and ending with instants on the provided second timeline. The result is Normalized.\n\n```\n\"source  \": \"|  |    |  \"\n\"instants\": \"   |  |   |\"\n\"result  \": \"\u003c  X  \u003e \u003c \u003e\"\n```\n\nExample with normalization:\n\n```\n\"source  \": \"|| |    |  \"\n\"instants\": \"   |  | | |\"\n\"result  \": \"\u003c  X  \u003e \u003c \u003e\"\n```\n\n#### AsConsecutivePeriodTimeline\n\nReturns a `IPeriodTimeline` with consecutive periods starting and ending with instants on the provided source timeline.\n\n```\n\"source \": \"|  |    |  \"\n\"result \": \"X  X    X  \"\n```\n\n### Transform IPeriodTimeline\n\n#### Cut\n\nReturns a `IPeriodTimeline` in which periods from the source are cut at provided instants.\n\n```\n\"source  \": \"\u003c    \u003e  \u003c \u003e\"\n\"instants\": \"|   |    | \"\n\"result  \": \"\u003c   X\u003e  \u003cX\u003e\"\n```\n\n#### Stitch\n\nReturns a `IPeriodTimeline` in which all periods in the source with equal end and start instants are combined into a single period.\n\n```\n\"source  \": \"\u003c   X\u003e  \u003cX\u003e\"\n\"result  \": \"\u003c    \u003e  \u003c \u003e\"\n```\n\n#### IntersectPeriods\n\nReturns a `IPeriodTimeline` with the intersections of the source with another `IPeriodTimeline`.\n\n```\n\"source\": \"\u003c   \u003e  \u003c  \u003e\"\n\"other \": \"  \u003c  \u003e\u003c  \u003e \"\n\"result\": \"  \u003c \u003e  \u003c \u003e \"\n```\n\n\u003e IntersectPeriods is also implemented for the `\u0026` operator.\n\n#### Invert\n\nReturns a `IPeriodTimeline` that is inverted of the provided source.\n\n```\n\"source\": \" \u003c  \u003e \u003c  \u003e \"\n\"result\": \" \u003e  \u003c \u003e  \u003c \"\n```\n\n#### Merge\n\nMerges all periods in from the source with all periods in another `IPeriodTimeline`. Overlapping periods are combined.\n\n```\n\"source\": \"\u003c \u003e   \u003c \u003e\u003c\u003e\"\n\"other \": \" \u003c  \u003e  \u003c  \u003e\"\n\"result\": \"\u003c   \u003e \u003c   \u003e\"\n```\n\n\u003e Merge is also implemented for the `+` and `|` operators.\n\n#### Subtract\n\nSubtracts all periods in a provided `IPeriodTimeline` from all periods in the source.\n\n```\n\"source    \": \"\u003c   \u003e  \u003c  \u003e\"\n\"subtrahend\": \"  \u003c   \u003e \u003c \u003e\"\n\"result    \": \"\u003c \u003e    \u003c\u003e  \"\n```\n\n\u003e Subtract is also implemented for the `-` operator.\n\n#### Offset\n\nOffsets the source with a given timespan.\n\n```\n\"source\": \"\u003c  \u003e  \u003c\u003e   \"\n\"offset\": 3\n\"result\": \"   \u003c  \u003e  \u003c\u003e\"\n```\n\n\u003e Offset is also implemented for the `+` and `-` operators in combination with a `TimeSpan`.\n\n#### Randomize\n\nRandomizes the source periods.\nThis method will never result in a change of period count or in overlapping periods.\nIdentical inputs with the same seed, will result in the same output.\n\n### Transform Multiple IPeriodTimeline\n\n#### WhereOverlapCount\n\nReturns a `IPeriodTimeline` with periods that start and end as the amount of overlapping periods from the provided periods trigger the predicate to become true or false.\n\n```\n\"source   \": \"    \u003c\u003e     \"\n             \"\u003c \u003e        \"\n             \"       \u003c   \"\n             \" \u003c\u003e      \u003c \"\n             \"\u003c         \u003e\"\n\"predicate\": n =\u003e n \u003e= 3\n\"result   \": \" \u003c\u003e      \u003c\u003e\"\n```\n\n```\n\"source   \": \"    \u003c\u003e     \"\n             \"\u003c \u003e        \"\n             \"       \u003c   \"\n             \" \u003c\u003e      \u003c \"\n             \"   \u003c   \u003e   \"\n\"predicate\": n =\u003e n \u003e 0 \u0026\u0026 n % 2 == 0\n\"result   \": \" \u003c\u003e \u003c\u003e   \u003c \"\n```\n\n#### ToOverlapTimelines\n\nReturns a Dictionary with the amount of overlapping periods in the provided periods.\n\n```\n\"source\":     \"\u003c         \u003e\"\n              \"   \u003c     \u003e \"\n              \"     \u003c   \u003e \"\n              \" \u003c\u003e    \u003c\u003e  \"\n\"result\": [1, \"\u003c\u003e\u003c\u003e     \u003c\u003e\"]\n          [2, \" \u003c\u003e\u003c \u003e     \"]\n          [3, \"     \u003c \u003e\u003c\u003e \"]\n          [4, \"       \u003c\u003e  \"]\n```\n\n## Important Considerations\n\nWhen applying filters to a timeline, the source timeline still needs to be evaluated upon enumeration. This is an important factor to consider when using a filter.\n\nFor example: If you apply a `Within` filter to a period timeline containing Mondays and use Fridays as a mask, every call to the filtered timeline will loop through both timelines when requesting an instant.\n\n```cs\nIPeriodTimeline mondays = TimeZonePeriods.Days(DayOfWeek.Monday);\nIPeriodTimeline fridays = TimeZonePeriods.Days(DayOfWeek.Friday);\n\nIPeriodTimeline mondaysWithinFridays = mondays.Within(fridays);\n\nDateTime? firstInstant = mondays.StartInstantProvider.GetNextUtcInstant(DateTime.UtcNow); // This method will eventually return null, but won't perform.\n```\n\n## Unit Tests\n\nThis library is extensively tested using unit tests.\n\nSince most tests follow a similar pattern, the ASCII notation is also utilized for consistency. The tests are dynamically loaded from JSON files containing test cases.\n\nHere’s an example snippet from `PeriodTimeline.Merge.json`:\n\n```json\n{\n  \"source  \": \"\u003c\u003e\u003c\u003e\",\n  \"periods \": \"\u003c   \",\n  \"expected\": \"\u003c   \"\n},\n{\n  \"source  \": \"\u003c\u003e\u003c\u003e\",\n  \"periods \": \" \u003c  \",\n  \"expected\": \"\u003cX  \"\n},\n{\n  \"source  \": \"\u003c\u003e\u003c\u003e\",\n  \"periods \": \"  \u003c \",\n  \"expected\": \"\u003c\u003e\u003c \"\n},\n```\n\nEach of these tests is executed at least three times: once for `GetPreviousUtcInstant`, once for `GetNextUtcInstant` and once for `IsInstant`.\n\n## Versioning and Stability\n\nThis library follows Semantic Versioning (SemVer), using the format MAJOR.MINOR.PATCH (x.y.z):\n\n- MAJOR (X) – Increases when there are breaking changes (starting from 1.0.0).\n- MINOR (Y) – Increases when new features are introduced (or breaking changes while still in 0.x).\n- PATCH (Z) – Increases when making backward-compatible bug fixes.\n\n⚠️ Since this library is still in version 0.x, breaking changes may occur even in minor version updates (e.g., 0.2.0 → 0.3.0). Until we reach a stable 1.0.0, updates may require adjustments to your code.\n\nFor ongoing discussions and future changes, check out [this GitHub discussion](https://github.com/Occurify/Occurify/discussions/4).\n\n## Release Notes\n\nSee the latest changes in [GitHub Releases](https://github.com/Occurify/Occurify/releases).\n\n## Multi-Language Support\n\nThe concepts behind Occurify are language-agnostic.\n\nThis repository currently contains the .NET implementation of Occurify. If you're interested in implementing this concept in another language (e.g. Java, Python, JavaScript), feel free to open an issue or reach out!\n\nIf a new implementation is created, this repo may be renamed to better organize multi-language versions.\n\n📢 Want to contribute a new language version? Let’s discuss it in the [discussions section](https://github.com/Occurify/Occurify/discussions)!\n\n## License\n\nCopyright © 2025 Jasper Lammers. Occurify is licensed under The MIT License (MIT).","funding_links":[],"categories":["🗒️ Cheatsheets","Scheduling","Identifiers"],"sub_categories":["📦 Libraries","GUI - other"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FOccurify%2FOccurify","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FOccurify%2FOccurify","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FOccurify%2FOccurify/lists"}