{"id":13483828,"url":"https://gitlab.com/crystallabs/crystime","last_synced_at":"2025-03-27T15:30:38.533Z","repository":{"id":63905101,"uuid":"6695078","full_name":"crystallabs/crystime","owner":"crystallabs","description":"Advanced time, calendar, schedule, and remind library for Crystal ","archived":false,"fork":false,"pushed_at":null,"size":null,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":null,"default_branch":"master","last_synced_at":"2024-10-30T17:48:21.287Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":null,"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}},"created_at":"2018-06-04T16:09:56.728Z","updated_at":"2018-09-05T19:27:28.815Z","dependencies_parsed_at":"2022-11-28T19:23:23.797Z","dependency_job_id":null,"html_url":"https://gitlab.com/crystallabs/crystime","commit_stats":null,"previous_names":[],"tags_count":2,"template":null,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/gitlab.com/repositories/crystallabs%2Fcrystime","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/gitlab.com/repositories/crystallabs%2Fcrystime/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/gitlab.com/repositories/crystallabs%2Fcrystime/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/gitlab.com/repositories/crystallabs%2Fcrystime/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/gitlab.com/owners/crystallabs","download_url":"https://gitlab.com/crystallabs/crystime/-/archive/master/crystime-master.zip","host":{"name":"gitlab.com","url":"https://gitlab.com","kind":"gitlab","repositories_count":4518295,"owners_count":6897,"icon_url":"https://github.com/gitlab.png","version":null,"created_at":"2022-05-30T11:31:42.605Z","updated_at":"2024-07-18T11:24:13.055Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/gitlab.com","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/gitlab.com/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/gitlab.com/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/gitlab.com/owners"}},"keywords":[],"created_at":"2024-07-31T17:01:15.689Z","updated_at":"2025-03-27T15:30:37.933Z","avatar_url":null,"language":null,"funding_links":[],"categories":["Scheduling"],"sub_categories":[],"readme":"**Build status**: [![Build Status](https://travis-ci.com/crystallabs/crystime.svg?branch=master)](https://travis-ci.com/crystallabs/crystime)\n[![Version](https://img.shields.io/github/tag/crystallabs/crystime.svg?maxAge=360)](https://github.com/crystallabs/crystime/releases/latest)\n[![License](https://img.shields.io/github/license/crystallabs/crystime.svg)](https://github.com/crystallabs/crystime/blob/master/LICENSE)\n\n**Project status**: `[ ] Being developed  [X] Usable  [ ] Functionally complete`\n\nCrystime is an advanced time, calendar, scheduling, and reminding library for Crystal.\n\nIt provides two classes: VirtualTime and Item.\n\n## VirtualTime\n\nThe basis of the low-level functionality is class \"VirtualTime\". Think of it as of\na normal \"Time\" struct, but much more flexible.\n\nWith regular Time, all fields (year, month, day, hour, minute, second, millisecond) must\nhave a value, and that value must be a specific number. Even if some of Time's fields don't\nrequire you to set a value (such as hour or minute values), they still default to 0\ninternally. As such, Time objects always represent specific dates (\"materialized\"\ndates in Crystime terminology).\n\nWith Crystime's VirtualTime, each field (year, month, day, hour, minute,\nsecond, millisecond, day of week, and [julian day](https://en.wikipedia.org/wiki/Julian_day))\ncan either remain unspecified, or be a number, or contain a more complex specification\n(list, range, range with step, boolean, or proc).\n\nFor example, you could construct a VirtualTime with a month of March and a day range\nof 10..20 with step 2. This would represent a \"virtual time\" that matches any Time or\nanother VirtualTime which falls on, or contains, the dates of March 10, 12, 14, 16, 18, or 20.\n\n## Item\n\nThe basis of the high-level user functionality is class \"Item\". This is intentionally\ncalled an \"item\" not to imply any particular type or purpose (e.g. a task, event,\nrecurring appointment, reminder, etc.)\n\nAn item has an absolute start and end VirtualTime, a list of VirtualTimes on which it is considered\n\"on\" (i.e. active, due, scheduled), a list of VirtualTimes on which it is specifically\n\"omitted\" (i.e. \"not on\", like on weekends, individual holidays dates, certain times of\nday, etc.),\nand a rule which specifies what to do if an event falls on an omitted date or time \u0026mdash;\nit can still be \"on\", or ignored, or re-scheduled to some time before, or some time after.\n\nIf the item's list of due dates is empty, it is considered as always \"on\".\nIf the item's list of omit dates is empty, it is considered as never omitted.\nIf there are multiple VirtualTimes set for a field, the matches are logically OR-ed,\ni.e. one match is enough for the field to match.\n\nHere is a simple example from the examples/ folder to begin with, with comments:\n\n```crystal\n# Create an item:\nitem = Crystime::Item.new\n\n# Create a VirtualTime that matches every other day from Mar 10 to Mar 20:\ndue_march = Crystime::VirtualTime.new\ndue_march.month = 3\ndue_march.day = (10..20).step 2\n\n# Add this VirtualTime as due date to item:\nitem.due\u003c\u003c due_march\n\n# Create a VirtualTime that matches Mar 20 specifically. We will use this to actually omit\n# the event on that day:\nomit_march_20 = Crystime::VirtualTime.new\nomit_march_20.month = 3\nomit_march_20.day = 20\n\n# Add this VirtualTime as omit date to item:\nitem.omit\u003c\u003c omit_march_20\n\n# If event falls on an omitted date, try rescheduling it for 2 days later:\nitem.omit_shift = Crystime::Span.new(86400 * 2)\n\n\n# Now we can check when the item is due and when it is not:\n\n# Item is not due on Feb 15, 2017 because that's not in March:\np item.on?( Crystime::VirtualTime[\"2017-02-15\"]) # ==\u003e false\n\n# Item is not due on Mar 15, 2017 because that's not a day of\n# March 10, 12, 14, 16, 18, or 20:\np item.on?( Crystime::VirtualTime[\"2017-03-15\"]) # ==\u003e false\n\n# Item is due on Mar 16, 2017:\np item.on?( Crystime::VirtualTime[\"2017-03-16\"]) # ==\u003e true\n\n# Item is due on Mar 18, 2017:\np item.on?( Crystime::VirtualTime[\"2017-03-18\"]) # ==\u003e true\n\n# And it is due on any Mar 18, doesn't need to be in 2017:\nany_mar_18 = Crystime::VirtualTime.new\nany_mar_18.month = 3\nany_mar_18.day = 18\np item.on?( any_mar_18 ) # ==\u003e true\n\n# We can check whether this event is due at any point in March:\nany_mar = Crystime::VirtualTime.new\nany_mar.month = 3\np item.on?( any_mar) # ==\u003e true\n\n# But item is not due on Mar 20, 2017, because that date is omitted, and the system will give us\n# a span of time (offset) when it can be scheduled. Based on our reschedule settings above, this\n# will be a span for 2 days later.\np item.on?( Crystime::VirtualTime[\"2017-03-20\"]) # ==\u003e #\u003cCrystime::Span @span=2.00:00:00\u003e\n\n# Asking whether the item is due on the rescheduled date (Mar 22) will tell us no, because currently\n# rescheduled dates are not counted as due/on dates:\np item.on?( Crystime::VirtualTime[\"2017-03-22\"]) # ==\u003e nil\n\n# We can also check whether the item is due using regular Time struct,\n# it does not have to be VirtualTime:\np item.on?( Time.new 2018, 3, 16) # ==\u003e true\n```\n\n# VirtualTime in Detail\n\nAll date/time objects in Crystime (due dates, omit dates, start/stop dates, dates to check etc.)\nare based on VirtualTime. That is because VirtualTime does everything Time does (except maybe\nproviding some convenience functions), so it is simpler and more powerful to use it everywhere.\n\n(If you are missing any particular convenience/compatibility functions from Time, please report\nthem or submit a PR.)\n\nA VirtualTime has the following fields that can be set in an initializer or after object creation:\n\n```\nyear        - Year value\nmonth       - Month value (1-12)\nday         - Day value (1-31)\n\nday_of_week - Day of week (Sunday = 0, Saturday = 6)\njd          - Julian Day Number\n\nhour        - Hour value (0-23)\nminute      - Minute value (0-59)\nsecond      - Second value (0-59)\nmillisecond - Millisecond value (0-999)\n```\n\nEach of the above listed fields can have the following values:\n\n1. Nil / undefined (matches everything it is compared with)\n1. A number that is native/accepted for a particular field, e.g. 1 or -2 (negative values count from the end)\n1. A list of numbers native/accepted for a particular field, e.g. [1, 2] or [1, -2] (negative values count from the end)\n1. A range, e.g. 1..6\n1. A range with a step, e.g. (1..6).step(2)\n1. True (inserts default values in place of 'true')\n1. A proc (accepts Int32 as arg, and returns Bool) (not tested extensively)\n\n## Weekday (Day of Week) and Julian Day Number\n\nThe day of week and [Julian Day Number](https://en.wikipedia.org/wiki/Julian_day) fields are in relation with the\nY/m/d values. One can't change one without triggering an automatic change in the other. Specifically:\n\nAs long as VirtualTime is materialized (i.e. has specific Y/m/d values), then changing\nany of those values will update `day_of_week` and `jd` automatically. Similarly, setting\nJulian Day Number will automatically update Y/m/d (and implicitly also day of week) and cause the\ndate to become materialized.\n\nPlease note that in the current behavior, setting `day_of_week` will not affect Y/m/d or jd.\nIn essence, if Y/m/d is set and day of week is later changed\nfrom its existing/auto-computed value, it will probably no longer match any date,\nbecause the date and day of week will be out of sync. This behavior\nmay be useful in rare circumstances (most probably generated by some\nautomated script) where you want to match a fixed date, but only if\nthat date also happens to fall on a specified day of week.\n\nPlease also note that in the current behavior, de-materializing a VT\nresets both `day_of_week` and `jd` to nil.\n\nThe current behavior was choosen based on the assumption that it would\nbe the most intuitive / most useful behavior. It could be changed if\nsome other behavior is deemed more appropriate.\n\nAltogether, the described syntax allows for specifying simple but functionally intricate\nrules, of which just some of them are:\n\n```\nday=-1                     -- matches last day in month\nday_of_week=6, day=24..31  -- matches last Saturday in month\nday_of_week=1..5, day=-1   -- matches last day of month if it is a workday\n```\n\nPlease note that these are individual VirtualTime rules. Complete Items\n(described below) can have multiple VirtualTimes set as their due, omit,\nand check dates, so really arbitrary rules can be expressed. \n\n## VirtualTime from String\n\nThere are two ways to create a VirtualTime and both have been implicitly shown\nin use above.\n\nOne is by invoking e.g. `vt = VirtualTime.new` and then setting the individual\nfields on `vt`.\n\nFor example:\n\n```crystal\nvt = Crystime::VirtualTime.new\n\nvt.year = nil # Remains unspecified, matches everything it is compared with\nvt.month = 3\nvt.day = [1,2]\nvt.hour = (10..20)\nvt.minute = (10..20).step(2)\nvt.second = true\nvt.millisecond = -\u003e( val : Int32) { true }\n```\n\nAnother is creating a VirtualTime from a string, using notation `vt = VirtualTime[\"... string ...\"]`.\nThis parser should eventually support everything supported by Ruby's `Time.parse`, `Date.parse`,\n`DateTime.parse`, etc., but for now it supports the following strings and their combinations:\n\n```\n# Year-Month-Day\nyyyy-mm?-dd?\nyyyy.mm?.dd?\nyyyy/mm?/dd?\n\n# Hour-Minute-Second-Millisecond\nhh?:mm?:ss?\nhh?:mm?:ss?:mss?\nhh?:mm?:ss?.mss?\n\n# Year\nyyyy\n\n# Month abbreviations\nJAN, Feb, ...\n\n# Day names\nMON, Tue, ...\n\n```\n\nFor example:\n\n```\nvt = VirtualTime[\"JAN 2018\"]\np vt.month # ==\u003e 1\n\nvt = VirtualTime[\"2018 sun\"]\np vt.day_of_week # ==\u003e 0\n\nvt = VirtualTime[\"2018 wed 12:00:00\"]\np vt.day_of_week # ==\u003e 3\n```\n\n## VirtualTime Materialization\n\nVirtualTimes sometimes need to be fully materialized for\nthe purpose of display, calculation, comparison, or conversion. An obvious such case\nis when `to_time()` is invoked on a VT, because a Time object must have all of its\nfields set.\n\nFor that purpose, each VirtualTime keeps track of which of its 7 fields (Y, m, d, H, M, S, and\nmillisecond) are set, and which of them are materializable. If any of the individual\nfields are not materializable, then the VT is not either, and an Exception is thrown\nif materialization is attempted.\n\nCurrently, unset values and specific integers are materializable, while fields containing\nany other specification are not.\n\nIn a call to `materialize!`, one can specify a \"hint\" argument, whose values will be used\nto aid the materialization process. E.g. to default dates to current date, to default\ntimes to 12:00 instead of to 00:00, to materialize ranges to something other than their\nbeginning, or to run procs which will return values produced in a custom way.\n\nCurrently, the implementation is simple, and hint's values are used verbatim in place of\n`nil`s in the original VT, so they are expected to be simple integers.\n\nFor example:\n\n```crystal\nvt= Crystime::VirtualTime.new\n\n# These fields will be used as-is:\nvt.year= 2018\nvt.day= 15\n\n# While others (nils) will be taken from \"hint\":\nhint= Crystime::VirtualTime.new 1,2,3,4,5,6,7\n\nvt.materialize!(hint).to_array # ==\u003e [2018,2,15,4,5,6,7]\n```\n\nFor convenience, the VT's ability to materialize each of its individual fields using their\ncurrent values can be checked through a getter named `ts`:\n\n```crystal\nvt = Crystime::VirtualTime.new\n\nvt.year = nil\nvt.month = 3\nvt.day = [1,2]\nvt.hour = (10..20)\nvt.minute = (10..20).step(2)\nvt.second = true\nvt.millisecond = -\u003e( val : Int32) { true }\n\n# Fields containing nil or true are materializable; fields containing false are not:\nvt.ts # ==\u003e [nil, true, false, false, false, false, false]\n```\n\n# Item in Detail\n\nAs mentioned, Item is the toplevel object representing a task/event/etc.\n\nIt does not contain any task/event-specific properties, it only concerns itself with\nthe scheduling aspect and has the following fields:\n\n```\nstart      - Start VirtualTime (item is never \"on\" before this date)\nstop       - End VirtualTime (item is never \"on\" after this date)\n\ndue        - List of due/on VirtualTimes\nomit       - List of omit/not-on VirtualTimes\n\nomit_shift - What to do if item falls on an omitted date/time:\n           - nil: treat it as not being \"on\"\n           - false: treat it as being \"on\", but falling on an omitted\n             and non-reschedulable date, so effectively it is not \"on\"\n           - true: treat it as \"on\", regardless of falling on omitted date\n           - Crystime::Span or Time::Span: attempt shifting (rescheduling) by specified span on\n             each attempt. The span to shift can be negative or positive for\n             shifting to an earlier or later date.\n\nshift      - List of VirtualTimes which the new proposed item time (produced by\n             shifting the date by omit_shift span in an attempt to reschedule it)\n             must match for the item to be considered \"on\"\n\n# (Reminder capabilities were previously in, but now they are\n# waiting for a rewrite and essentially aren't available.)\n```\n\nHere's an example of an item that's due every other day in March, but if it falls\non a weekend it is ignored. (This is also one from the examples/ folder.)\n\n```crystal\n# Create an item:\nitem = Crystime::Item.new\n\n# Create a VirtualTime that matches every other day in March:\ndue_march = Crystime::VirtualTime.new\ndue_march.month = 3\ndue_march.day = (2..31).step 2\n# Add this VirtualTime as due date to item:\nitem.due\u003c\u003c due_march\n\n# But on weekends it should not be scheduled:\nnot_due_weekend = Crystime::VirtualTime.new\nnot_due_weekend.day_of_week = [0,6]\n# Add this VirtualTime as omit date to item:\nitem.omit\u003c\u003c not_due_weekend\n\nitem.omit_shift = nil\n\n# Now let's check when it is due and when not:\n(1..31).each do |d|\n  p \"2017-03-#{d} = #{item.on?( Crystime::VirtualTime[\"2017-03-#{d}\"])}\"\nend\n```\n\n# Additional Info\n\nAll of the features are covered by specs, please see spec/* for more ideas\nand actual, working examples. To run specs, run the usual command:\n\n```\ncrystal spec\n```\n\nIn addition to that, also check the examples in the folder `examples/`.\n\n# TODO\n\n1. Add reminder functions. Previously remind features were implemented using their own code/approach. But maybe reminders should be just regular Items whose exact due date/time is certain offset from the original Item's date/time.\n1. Currently, there is good code for inserting default values if field's value is \"true\", but there is no ways for users to fill in those defaults\n1. Add more cases in which a VirtualTime is materializable (currently it is not if any of its values are anything else other than unset or a number). This should work with the help of user-supplied VT as argument, which will provide hints how to materialize objects in case of ambiguities or multiple choices.\n1. Add more features suitable to be used in a reimplementation of cron using this module\n1. Add a rbtree or something, sorting the items in order of most recent to most distant due date\n1. Possibly add some support for triggering actions on exact due dates of items/reminders\n1. Implement a complete task tracking program using Crystime\n1. Write support for exporting items into other calendar apps\n1. Proc in VT values should be able to accept all value types that a VT field can accept\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/gitlab.com%2Fcrystallabs%2Fcrystime","html_url":"https://awesome.ecosyste.ms/projects/gitlab.com%2Fcrystallabs%2Fcrystime","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/gitlab.com%2Fcrystallabs%2Fcrystime/lists"}