{"id":15713683,"url":"https://github.com/crystallabs/virtualtime","last_synced_at":"2025-05-12T20:46:12.613Z","repository":{"id":66956514,"uuid":"123111987","full_name":"crystallabs/virtualtime","owner":"crystallabs","description":"Advanced time, calendar, schedule, and remind library for Crystal","archived":false,"fork":false,"pushed_at":"2024-01-30T11:42:57.000Z","size":286,"stargazers_count":22,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-01T03:17:23.667Z","etag":null,"topics":["calendar","crystal","crystal-lang","remind","schedule","time"],"latest_commit_sha":null,"homepage":"","language":"Crystal","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/crystallabs.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}},"created_at":"2018-02-27T10:19:12.000Z","updated_at":"2024-08-12T19:36:33.000Z","dependencies_parsed_at":"2023-12-18T18:50:45.888Z","dependency_job_id":"758c3327-152c-48f8-b650-57eca2526299","html_url":"https://github.com/crystallabs/virtualtime","commit_stats":null,"previous_names":["crystallabs/virtualtime"],"tags_count":20,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crystallabs%2Fvirtualtime","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crystallabs%2Fvirtualtime/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crystallabs%2Fvirtualtime/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/crystallabs%2Fvirtualtime/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/crystallabs","download_url":"https://codeload.github.com/crystallabs/virtualtime/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253818804,"owners_count":21969225,"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","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":["calendar","crystal","crystal-lang","remind","schedule","time"],"created_at":"2024-10-03T21:32:53.568Z","updated_at":"2025-05-12T20:46:12.590Z","avatar_url":"https://github.com/crystallabs.png","language":"Crystal","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Linux CI](https://github.com/crystallabs/virtualtime/workflows/Linux%20CI/badge.svg)](https://github.com/crystallabs/virtualtime/actions?query=workflow%3A%22Linux+CI%22+event%3Apush+branch%3Amaster)\n[![Version](https://img.shields.io/github/tag/crystallabs/virtualtime.svg?maxAge=360)](https://github.com/crystallabs/virtualtime/releases/latest)\n[![License](https://img.shields.io/github/license/crystallabs/virtualtime.svg)](https://github.com/crystallabs/virtualtime/blob/master/LICENSE)\n\n# VirtualTime\n\nVirtualTime is a Time-related class for Crystal. It is used for matching and generation of compliant dates and times, primarily for calendar, scheduling, and reminding purposes.\n\n## Installation\n\nAdd the following to your application's \"shard.yml\":\n\n```\ndependencies:\n  virtualtime:\n    github: crystallabs/virtualtime\n    version: ~\u003e 1.0\n```\n\nAnd run `shards install` or just `shards`.\n\n## Overview of Functionality\n\nAs mentioned, VirtualTime is used for matching and generation of `Time`s.\n\n### 1. Matching Times\n\nYou can express date and time constraints in the `VirtualTime` object and then match various `Time`s against it\nto determine which ones match.\n\nFor example, let's create a VirtualTime that matches the last Saturday and Sunday of every month.\nThis can be expressed using two constraints:\n\n- Day of month should be between -8 and -1 (the last 7 days of any month)\n- Day of week should be 6 or 7 (Saturday and Sunday)\n\n```cr\nvt = VirtualTime.new\nvt.day = -8..-1\nvt.day_of_week = [6,7]\n\n# Check if current time matches\nvt.matches?(Time.local) # =\u003e result depends on current time\n```\n\n### 2. Matching VirtualTimes\n\nIn addition to matching `Time`s, it is also possible to match `VirtualTime`s against each other.\n\nLet's say we are interested in knowing whether the above VT would match any day in the month of March.\n\nWe could do this with:\n\n```cr\n# Same VT as before:\nvt = VirtualTime.new\nvt.day = -8..-1\nvt.day_of_week = [6,7]\n\n# Check if the specified VT matches any day in month of March\nany_in_march = VirtualTime.new month: 3\nvt.matches?(any_in_march) # =\u003e true\n```\n\nNote that `#matches?` is commutative and it could have also been written as `any_in_march.matches?(vt)`.\n\n### 3. Time Generation\n\nIn addition to matching, it is also possible to successively generate `Time`s that match the specified\nVirtualTime constraints.\n\nFor example, let's take the same `VirtualTime` as above which matches the last weekend days of every month,\nand print a list of next 10 such dates:\n\n```cr\nvt = VirtualTime.new\nvt.year = 2020..2030\nvt.day = -7..-1\nvt.day_of_week = [6,7]\n\nvti = vt.step(1.day)\n\n10.times do\n  p vti.next\nend\n\n# 2024-01-27 11:16:00.0 +01:00 Local\n# 2024-01-28 11:16:00.0 +01:00 Local\n# 2024-02-24 11:16:00.0 +01:00 Local\n# 2024-02-25 11:16:00.0 +01:00 Local\n# 2024-03-30 11:16:00.0 +01:00 Local\n# 2024-03-31 12:16:00.0 +02:00 Local\n# 2024-04-27 12:16:00.0 +02:00 Local\n# 2024-04-28 12:16:00.0 +02:00 Local\n# 2024-05-25 12:16:00.0 +02:00 Local\n# 2024-05-26 12:16:00.0 +02:00 Local\n```\n\n## Supported Property Values\n\nCrystal's `struct Time` has all its fields (year, month, day, hour, minute, second, nanosecond) set\nto a specific numeric value. Even if some of its fields aren't required in the constructor,\ninternally they still get initialized to 0, 1, or other suitable value.\n\nAs such, `Time` instances always represent specific dates and times (\"materialized\" dates and times\nin virtualtime's terminology).\n\nOn the other hand, `VirtualTime`s do not have to represent any specific points in time (although they can\nbe defined precisely enough (or converted) so that they do).\nThey are primarily intended for conveniently matching broader sets of values.\n\nAll VirtualTime instances contain the following properties:\n\n1. **Year** (0..9999)\n1. **Month** (1..12)\n1. **Day** (1..31)\n1. **Week number of year** (0..53)\n1. **Day of week** (1..7, Monday == 1)\n1. **Day of year** (1..366)\n1. **Hour** (0..23)\n1. **Minute** (0..59)\n1. **Second** (0..59)\n1. **Millisecond** (0..999)\n1. **Nanosecond** (0..999_999_999)\n\nAnd each of these properties can have a value of the following types:\n\n1. **Nil**, to default to `VirtualTime.default_match? : Bool = true`\n1. **Boolean**, to always match (`true`) or fail (`false`)\n1. **Int32**, to match a specific value such as 5, 12, 2023, -1, or -5\n1. **Array or Set of Int32s**, such as [1,2,10,-1] to match any value in list\n1. **Range of Int32..Int32**, such as `10..-1` to match any value in range\n1. **Range with step**, e.g. `day: (10..20).step(2)`, to match any value in range with step\n1. **Proc**, to match a value if the return value from calling a proc is `true`\n\nAll properties (that are specified, i.e. not nil) must match for the match to succeed.\nProperties that *are* nil will match depending on the value of `#default_match?`.\n\nKnowing the structure of `VirtualTime` now, let's create a more elaborate example, with\ndescriptions included inline:\n\n```cr\nvt = VirtualTime.new\nvt.month = 3                # Month of March\nvt.day = [1,-1]             # First and last day of every month\nvt.hour = (10..20)          # Hour between 10 and 20, inclusively\nvt.minute = (0..59).step(2) # Every other (even) minute in an hour\nvt.second = true            # Unconditional match\nvt.millisecond = -\u003e( val : Int32) { true } # Unconditional match, since block returns true\nvt.location = Time::Location.load(\"Europe/Amsterdam\")\n\nvt.matches?(Time.local) # =\u003e result depends on current time\n```\n\n## Level of Granularity\n\nVirtualTime performs all internal calculations using maximum precision available from the\n`Time` struct (which is nanoseconds), but since the primary intended usage is for human\nscheduling, the default displayed granularity is 1 minute, with seconds and\nnanoseconds defaulting to 0.\n\nTo increase granularity, simply specify interval and step arguments manually (e.g. `1.second`) instead of defaulting to `1.minute`.\n\nIn other cases, the default interval of 1 minute could be too small. For example,\nif VirtualTime was created with only the `hour` value specified, it would match (and also\ngenerate) and event on every minute of that hour.\n\nIn that case, you could easily request the step to be e.g.  1 hour or 1 day, so that\nthere would be reasonable space between the generated `Time`s.\n\nFor example:\n\n```cr\nvt = VirtualTime.new\nvt.year = 2020..2030\nvt.day = -8..-1\nvt.day_of_week = [6,7]\n\nvti = vt.step(1.minute)\n2.times do p vti.next end\n# 2024-01-27 11:16:00.0 +01:00 Local\n# 2024-01-27 11:17:00.0 +01:00 Local\n\nvti = vt.step(1.day)\n2.times do p vti.next end\n# 2024-01-27 11:16:00.0 +01:00 Local\n# 2024-01-28 11:16:00.0 +01:00 Local\n```\n\n## Property Values in Detail\n\nAs can be seen above, fields can have some interesting values, such as negative numbers.\n\nHere is a list of all non-obvious values that are supported:\n\n### Negative Integer Values\n\nNegative integer values count from the end of the range, if the max / wrap-around value is\nspecified. Typical end values are 7, 12, 30/31, 365/366, 23, 59, and 999, and virtualtime\nimplicitly knows which one to apply in every case.\nFor example, a day of `-1` would always match the last day of the month, be that 28th, 29th,\n30th, or 31st in a particular case.\n\nIf the wrap-around value is not specified, negative values are not converted to positive\nones, and they enter matching as-is. In practice, this means they will not match any `Time`s,\nbut may match similar `VirtualTime`s.\n\nIt is also possible to use negative values in ranges, as explained next.\n\n### Range Values\n\nCrystal allows one to define `Range`s that have `end` value smaller than `begin`.\nSuch objects will simply not contain any elements.\n\nBecause creating such ranges *is* allowed, VirtualTime detects such cases and creates\ncopies of objects with values converted to positive and in the correct order.\n\nIn other words, if you specify a range of say, `day: (10..-7).step(2)`, this will properly\nmatch every other day from 10th to a day 7 days before the end of a month.\n\n### Week Numbers\n\nAnother interesting case are week numbers, which are calculated as number of Mondays in the year.\nThe first Monday in a year starts week number 1. But not every year starts on Monday, so up to\nthe first 3 days of a new year can still technically belong to the last week of the previous year.\n\nTherefore, this field can have values between 0 and 53 inclusively.\nValue 53 indicates a week that has started in one year (53rd Monday seen in a year),\nbut at least one (and up to 3) of its days will surely overflow into the new year.\n\nSimilarly, a value 0 matches up to the first 3 days (which inevitably must be Friday, Saturday,\nand/or Sunday) of the new year that belong to the week started in the previous year.\n\nNote: if you want to match the first or last 7 days of a year irrespective of weeks, you\nshould use `day: 1..7` or `day: -7..-1` instead.\n\n### Days in Month and Year\n\nFor `VirtualTime` objects, helper functions `days_in_month` and `days_in_year` return `0`.\n\nAs a consequence, when matching `VirtualTime`s to other `VirtualTime`s, any negative values remain negative and are matched directly.\n\nThis choice was made because it is only possible to know the number of days in a month\nif both `year` and `month` are defined and contain integers.\nIf they are not both defined, or they contain a value of any other type (e.g. a range\n`2023..2030`), it is ambiguous or indeterminable what the exact value should be.\nSo comparing VTs to VTs is always done without conversion of negative values.\n\n### Unsupported Comparisons\n\nComparisons between VirtualTime property values which are both a `Proc` are not supported\nand will throw `ArgumentError` in runtime.\n\nComparisons between VirtualTime objects with different `location` values are not supported\nand will throw `ArgumentError` in runtime.\n\n## Materialization\n\n\"Materialization\" is a process of converting all `VirtualTime`'s field values to specific\nintegers.\n\nVirtualTimes often need to be materialized for display, calculation, comparison,\nor further conversion.\n\nAn obvious such case is when `to_time()` is invoked on a VT, because a Time object must have\nall of its fields set to some integer value.\n\n(The difference between `#materialize` and `#to_time` is that materialize produces another VT with its fields materialized, while to_time produces a `Time` instance.)\n\nBecause VirtualTimes can be very broadly defined, often times there are many equal\nchoices to which they can be materialized. For example, if a VT matches anything in the\nmonth of March, which specific value should it be materialized to?\n\nTo avoid the problem of too many choices, materialization takes as an argument a `Time` hint which defaults to `Time.local`.\nThe materialized time will be equal to that time or moved to the future to satisfy all VT's constraints.\n\nFor example:\n\n```crystal\nvt = VirtualTime.new\n\n# These fields will be used as-is since they have a value:\nvt.year = 2018\nvt.day = 15\nvt.hour = 0\n\n# While others (which are nil) will have their value inserted from the \"hint\" object:\nhint = Time.local # 2023-12-09 12:56:26.837441132 +01:00 Local\n\nvt.materialize(hint).to_tuple # =\u003e {2018, 12, 15, nil, nil, nil, 0, 56, 26, nil, 837441132, nil}\np vt.to_time(hint) # =\u003e 2018-02-15 00:56:26.837441132 +01:00 Local\n```\n\nIf not specified, the time hint defaults to current local time.\n\n## Time Zones\n\n`VirtualTime` is timezone-agnostic. Values are compared against `VirtualTime` values as-is.\n\nHowever, `VirtualTime` has property `#location` which, if set and different than `Time`'s\n`#location`, will cause the time to be duplicated and have its timezone converted to\n`VirtualTime`'s location before matching.\n\nFor example:\n\n```cr\nvt = VirtualTime.new\nvt.hour = 16..20\n\nt = Time.local 2023, 10, 10, hour: 18, location: Time::Location.load(\"America/New_York\")\nvt.matches?(t) # =\u003e true, because hours `16..20` include hour `18`\n\nt = Time.local 2023, 10, 10, hour: 0, location: Time::Location.load(\"Europe/Berlin\")\nvt.matches?(t) # =\u003e nil, because 00 hours is not between 16 and 20\n\nvt.location = Time::Location.load(\"America/New_York\")\nvt.matches?(t) # =\u003e true, because time instant 0 hours converted to NY time (-6) is 18 hours\n```\n\nMatching VTs to VTs with timezones is also possible as long as the timezone is equal; otherwise a runtime error is thrown as already mentioned above under \"Unsupported Comparisons\".\n\n## Durations\n\nVirtualTime objects are not designed or intended to represent durations.\n\nWhile this may seem possible at first, for example by specifying\n`#hour = 11..13`, it is not generally viable because there is no way to\ndefine a duration of e.g. 2.5 hours from 11:00 to 13:30.\n\nFor such higher level constructs, see https://github.com/crystallabs/virtualdate.\n\n## Tests\n\nRun `crystal spec` or just `crystal s`.\n\n## API Documentation\n\nRun `crystal docs` or `crystal do` and `firefox ./docs/index.html`.\n\n## Other Projects\n\nList of interesting or similar projects in no particular order:\n\n- https://dianne.skoll.ca/projects/remind/ - a sophisticated calendar and alarm program\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcrystallabs%2Fvirtualtime","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcrystallabs%2Fvirtualtime","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcrystallabs%2Fvirtualtime/lists"}