{"id":44125258,"url":"https://github.com/rremple/intervalidus","last_synced_at":"2026-02-21T00:05:24.756Z","repository":{"id":254023598,"uuid":"844266383","full_name":"rremple/intervalidus","owner":"rremple","description":"For all your interval-based data needs.","archived":false,"fork":false,"pushed_at":"2026-02-17T04:09:20.000Z","size":9390,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-02-17T04:54:27.623Z","etag":null,"topics":["data","intervals"],"latest_commit_sha":null,"homepage":"https://rremple.github.io/intervalidus/latest/api/intervalidus","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rremple.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"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":"2024-08-18T21:54:14.000Z","updated_at":"2026-02-17T04:09:23.000Z","dependencies_parsed_at":"2024-11-25T11:31:07.305Z","dependency_job_id":"1ddf8337-d391-44a6-807c-c908a94052c1","html_url":"https://github.com/rremple/intervalidus","commit_stats":null,"previous_names":["rremple/intervalidus"],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/rremple/intervalidus","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rremple%2Fintervalidus","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rremple%2Fintervalidus/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rremple%2Fintervalidus/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rremple%2Fintervalidus/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rremple","download_url":"https://codeload.github.com/rremple/intervalidus/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rremple%2Fintervalidus/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29668652,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-20T23:24:07.480Z","status":"ssl_error","status_checked_at":"2026-02-20T23:24:06.202Z","response_time":59,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["data","intervals"],"created_at":"2026-02-08T21:16:47.543Z","updated_at":"2026-02-21T00:05:24.749Z","avatar_url":"https://github.com/rremple.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Intervalidus\n\n[![Scala CI](https://github.com/rremple/intervalidus/actions/workflows/scala.yml/badge.svg)](https://github.com/rremple/intervalidus/actions/workflows/scala.yml)\n[![Release](https://img.shields.io/github/v/release/rremple/intervalidus)](https://github.com/rremple/intervalidus/releases)\n[![Scala 3](https://img.shields.io/badge/Scala-3_LTS-blue)](https://www.scala-lang.org)\n[![Binary Compatibility](https://img.shields.io/badge/MiMa-helping-blue)](https://github.com/scalacenter/tasty-mima)\n[![Scala Steward](https://img.shields.io/badge/Scala_Steward-helping-blue.svg)](https://scala-steward.org)\n\n## In what intervals are your data valid?\n\nA Scala library with zero dependencies for representing data as valid only in discrete or continuous intervals, in one,\ntwo, three, or more (!) dimensions. This seems to come up a lot in application design both in terms of effective dating\nand versioning data.\n\n## Usage\n\nAdd the following to your **build.sbt** file:\n\n```sbt\nresolvers += \"Intervalidus\" at \"https://maven.pkg.github.com/rremple/intervalidus\"\nlibraryDependencies += \"rremple\" %% \"intervalidus\" % \"\u003cversion\u003e\"\n```\n\nFor more on usage including other artifacts, Scala 2 considerations, GitHub Package authentication, etc., see \n[expanded usage](usage.md).\n\n### Goals, Non-Goals, Background, and Motivation:\n\nThis is more of a personal exploration than anything. Mostly just revisiting my many decades of past enterprise\nsoftware projects and thinking about what generic libraries would have been useful. Also exploring the new capabilities\nand cleaner syntax of Scala 3 to solve real-world problems elegantly. This is a very generic, low-level library that\nsolves a very narrowly defined problem. (If you were looking for yet another cool application of AI that claims to solve\neverything wrong in the world, look elsewhere…)\n\nIn my experience, there are lots of ways to represent and collect commonly used data types. For example, just using the\ntypes that come with the Java/Scala standard libraries and other popular libraries (i.e., primitive types, strings, time\ntypes, various collections, etc.) is great. But the notion of how to express the conditions under which data are valid\n(not to be confused with validating data), although it comes up in project after project, is generally left as an\napplication design consideration. But it is such a common pattern, it is odd that there isn’t some more direct support\nin libraries for representing validity as a kind of collection. There are lots of libraries for representing intervals\nand even interval sets, but not intervals as they relate to data as a kind of collection, and not multidimensional\nintervals. (Intervalidus has no upper bound on the number of dimensions!)\n\nFor example, what is valid may depend on time. Say that on December 25\u003csup\u003eth\u003c/sup\u003e a user signs up for a basic tier of\nservice effective January 1\u003csup\u003est\u003c/sup\u003e. Then, on March 15\u003csup\u003eth\u003c/sup\u003e, they upgrade to the premium service tier\neffective on April 1\u003csup\u003est\u003c/sup\u003e. Later, on June 28\u003csup\u003eth\u003c/sup\u003e, they choose not to renew and cancel their service,\nexpecting it to expire at the end of the month. The following shows what tier is known to be valid, and how that evolves\nover time. (Here we use +∞ to represent no planned termination, or what is sometimes called the “end of time”.)\n\n| On    | Evolving valid tier data               |\n|-------|----------------------------------------|\n| 12/25 | Basic: 1/1 – +∞                        |\n| 3/15  | Basic: 1/1 – 3/31; Premium: 4/1 – +∞   |\n| 6/28  | Basic: 1/1 – 3/31; Premium: 4/1 – 6/30 |\n\nIntervalidus provides composable multidimensional data structures, both mutable and\nimmutable, to address storage and management of data like this. For more information, see the\n[full API documentation](https://rremple.github.io/intervalidus/api/intervalidus)\n### Usage:\n\nYou could use Intervalidus `Data` to represent the above as a one-dimensional structure that treats dates as \ndiscrete values like this:\n\n```scala 3\nimport java.time.LocalDate.of as date\nimport intervalidus.DiscreteValue.given\nimport intervalidus.Interval1D.*\nimport intervalidus.mutable.Data\n\nval plan1d = Data.of(intervalFrom(date(2024, 1, 1)) -\u003e \"Basic\")\nplan1d.set(intervalFrom(date(2024, 4, 1)) -\u003e \"Premium\")\nplan1d.remove(intervalFromAfter(date(2024, 6, 30)))\nprintln(plan1d)\n```\n\nWhich outputs a handy little Ascii Gantt of the valid values in the structure:\n\n```text\n| 2024-01-01 .. 2024-03-31 | 2024-04-01 .. 2024-06-30 |\n| Basic                    |\n                           | Premium                  |\n```\n\nWe could have just as easily used the immutable variant of `Data`, and the output would have been the same as\nthe above output:\n\n```scala 3\nimport java.time.LocalDate\nimport java.time.LocalDate.of as date\nimport intervalidus.DiscreteValue.given\nimport intervalidus.Interval1D.*\nimport intervalidus.immutable.Data\n\nval plan1d: Data.In1D[String, LocalDate] = Data\n  .of(intervalFrom(date(2024, 1, 1)) -\u003e \"Basic\")\n  .set(intervalFrom(date(2024, 4, 1)) -\u003e \"Premium\")\n  .remove(intervalFromAfter(date(2024, 6, 30)))\nprintln(plan1d)\n```\n\nSince `Data` is a [partial function](https://docs.scala-lang.org/scala3/book/fun-partial-functions.html),\nyou can query individual valid values using that interface (many other query methods exist as well). For example, using\nfunction application:\n\n```scala 3 \nplan1d(date(2024, 3, 15))\n```\n\nwill return `Basic` because the user had the Basic tier on 3/15.\n\nYou can query the structure for all the distinct values independent of their validity intervals:\n```scala 3 \nprintln(plan1d.values) // prints Set(Basic, Premium)\n```\n\nOr you can query by value to discover the interval(s) in which a particular value is valid:\n```scala 3 \nprintln(plan1d.intervals(\"Basic\")) // prints List([2024-01-01..2024-03-31])\n```\n\nGoing deeper, you may want two dimensions of time: one to track what is effective and one to track when these facts\nwere known (sometimes referred to as [bitemporal modeling](https://en.wikipedia.org/wiki/Bitemporal_modeling)).\nFor that, you could use the same Intervalidus `Data` to represent the above as a two-dimensional structure like\nthis:\n\n```scala 3\nimport java.time.LocalDate\nimport java.time.LocalDate.of as date\nimport intervalidus.DiscreteValue.given\nimport intervalidus.Interval1D.*\nimport intervalidus.immutable.Data\n\nval plan2d: Data.In2D[String, LocalDate, LocalDate] = Data\n  .of((intervalFrom(date(2024, 1, 1)) x intervalFrom(date(2023, 12, 25))) -\u003e \"Basic\")\n  .set((intervalFrom(date(2024, 4, 1)) x intervalFrom(date(2024, 3, 15))) -\u003e \"Premium\")\n  .remove(intervalFromAfter(date(2024, 6, 30)) x intervalFrom(date(2024, 6, 28)))\nprintln(plan2d)\n```\n\nWhich results in the following (slightly less straightforward) output:\n\n```text\n| 2024-01-01 .. 2024-03-31         | 2024-04-01 .. 2024-06-30         | 2024-07-01 .. +∞                 |\n| Basic [2023-12-25..2024-03-14]                                                                         |\n| Basic [2024-03-15..+∞)           |\n                                   | Premium [2024-03-15..2024-06-27]                                    |\n                                   | Premium [2024-06-28..+∞)         |\n```\n\nHere, the second time dimension is shown next to each valid value. Reading this line by line, the interpretation is:\n\n- From 12/25/2023 until 3/14, the user was known to have the Basic tier effective from 1/1, without any planned\n  termination.\n- From 3/15 and thereafter, the user was known to have the Basic tier effective only from 1/1 until 3/31.\n- Also from 3/15 until 6/27, the user was known to have the Premium tier effective from 4/1, without any planned\n  termination.\n- From 6/28 and thereafter, the user was known to have the Premium tier effective only from 4/1 until 6/30.\n\nSince decoding the `toString` output of `Data` can get complicated as more intervals are present, there is a utility\n(in the test package) called `Visualize2D` that can help with debugging and testing tasks. It uses 2D graphics to render\nthe horizontal and vertical dimensions more clearly. For example, `Visualize2D(plan2d)` displays the following, which is a\nbit easier to decipher:\n\n![2D data visualization](/doc/intervalidus-visualize.png)\n\n(A similar `Visualize3D` is provided for visualizing 3D data. It is a [Three.js](https://threejs.org/) app -- 100% \nvibe-coded using Gemini 2.5 Pro Preview 05-06. It renders the non-metric representation of data, allowing it\nto be rotated, sliced, and understood.)\n\nOne might query this structure to find what the August forecast was at various sampled dates in\nthe past (or future). For example, leveraging `plan2d` as a partial function (with an `unapply`):\n\n```scala 3\nval futureEffectiveDate: Domain1D[LocalDate] = date(2024, 8, 1)\nval knownDates = List(date(2023, 12, 15), date(2024, 1, 15), date(2024, 5, 15), date(2024, 7, 15))\n\nknownDates.foreach: knownDate =\u003e\n  futureEffectiveDate x knownDate match\n    case plan2d(tier) =\u003e println(s\"On $knownDate, forecasted $tier tier on $futureEffectiveDate\")\n    case _            =\u003e println(s\"On $knownDate, no forecasted tier on $futureEffectiveDate\")\n```\n\nThe result shows how this August forecast changed over time:\n\n```text\nOn 2023-12-15, no forecasted tier on 2024-08-01\nOn 2024-01-15, forecasted Basic tier on 2024-08-01\nOn 2024-05-15, forecasted Premium tier on 2024-08-01\nOn 2024-07-15, no forecasted tier on 2024-08-01\n```\n\nOr to see all the effective-dated information on these known dates, we can extract one-dimensional structures\n(effective dates form dimension index 0, and known dates form dimension index 1).\n```scala 3\ndef plan2dOn(knownDate: LocalDate): Data.In1D[String, LocalDate] =\n  plan2d.getByDimension(dimensionIndex = 1, knownDate)\nknownDates.foreach: knownDate =\u003e\n  println(s\"On $knownDate:\\n${plan2dOn(knownDate).toString}\")\n```\n\nThe result shows how `plan2d` keeps a complete history of all one-dimensional effective periods:\n\n```text\nOn 2023-12-15:\n\u003cnothing is valid\u003e\n\nOn 2024-01-15:\n| 2024-01-01 .. +∞ |\n| Basic            |\n\nOn 2024-05-15:\n| 2024-01-01 .. 2024-03-31 | 2024-04-01 .. +∞         |\n| Basic                    |\n                           | Premium                  |\n\nOn 2024-07-15:\n| 2024-01-01 .. 2024-03-31 | 2024-04-01 .. 2024-06-30 |\n| Basic                    |\n                           | Premium                  |\n```\n\nGoing further, let's say tracking the known date is not enough. There are transactions that drive this new knowledge, \nand those transactions are timestamped. Then using an interval in the \"knowledge\" dimension based on a datetime rather\nthan a date would be more appropriate. But timestamps are better thought of as continuous rather than discrete values.\nSo we can use a continuous value in the knowledge dimension, and continue to use a discrete value in the effective date\ndimension.\n\n```scala 3\nimport java.time.LocalDate.of as date\nimport intervalidus.DiscreteValue.LocalDateDiscreteValue\nimport intervalidus.ContinuousValue.LocalDateTimeContinuousValue\nimport intervalidus.Interval1D.*\nimport intervalidus.immutable.Data\n\nval plan2d = Data\n  .of((intervalFrom(date(2024, 1, 1)) x intervalFrom(date(2023, 12, 25).atTime(10, 23, 33, 123456789))) -\u003e \"Basic\")\n  .set((intervalFrom(date(2024, 4, 1)) x intervalFrom(date(2024, 3, 15).atTime(14, 10, 15, 987654321))) -\u003e \"Premium\")\nprintln(plan2d)\n```\n\nWhich results in the following output, which is similar to what was shown in the previous example:\n\n```text\n| 2024-01-01 .. 2024-03-31                                             | 2024-04-01 .. +∞                                                     |\n| Basic [2023-12-25T10:23:33.123456789, 2024-03-15T14:10:15.987654321)                                                                        |\n| Basic [2024-03-15T14:10:15.987654321, +∞)                            |\n                                                                       | Premium [2024-03-15T14:10:15.987654321, +∞)                          |\n```\n\nThe two main differences are:\n- The timestamps are included in the knowledge dimension.\n- The notation for the intervals in this dimension is different. The boundary of each interval can either be\n  closed (denoted with brackets) or open (denoted with parens), and the start and end of each interval is separated by\n  a comma rather than two dots. \n\n(How discrete and continuous domain values differ in behavior is discussed later.)\n\nThese notational differences are carried in the representation of the horizontal dimension too. For example, if we flip\nthe horizontal and vertical dimensions (using pattern matching) and print the result:\n\n```scala 3\nimport intervalidus.Interval.Patterns.*\n\nval plan2dFlip = plan2d.mapIntervals:\n  case horizontal x_: vertical =\u003e vertical x horizontal\nprintln(plan2dFlip)\n```\n\nThe result shows how the continuous notation is pulled to the top and the discrete notation is used with each piece of \neffective-dated data:\n\n```text\n[ 2023-12-25T10:23:33.123456789, 2024-03-15T14:10:15.987654321 ) [ 2024-03-15T14:10:15.987654321, +∞ )                            |\n| Basic [2024-01-01..2024-03-31]                                                                                                  |\n| Basic [2024-04-01..+∞)                                         |\n                                                                 | Premium [2024-04-01..+∞)                                       |\n```\n\nThere is a sample billing application provided that demonstrates how both of these 1D and 2D structures could be used to \nsupport time-oriented logic, like billing, directly, including prospective calculation and retrospective\nadjustments/refunds.\n\nSometimes one wants to treat multiple values as valid in the same interval. For example, a product team may add/remove\nfeatures in each numbered release of a product. `DataMulti` could be used to model what features belong in what\nreleases, this time using intervals based on integers rather than dates.\n```scala 3\nimport intervalidus.DiscreteValue.given\nimport intervalidus.Interval1D.*\nimport intervalidus.immutable.DataMulti\n\ncase class Feat(id: String)\n\nval multi = DataMulti.In1D[Feat,Int]()\n  .addOne(intervalFrom(1) -\u003e Feat(\"A\")) // release 1 includes only feature A\n  .addOne(intervalFrom(2) -\u003e Feat(\"B\")) // release 2 adds feature B\n  .addOne(intervalFrom(3) -\u003e Feat(\"C\")) // release 3 adds feature C...\n  .removeOne(intervalFrom(3) -\u003e Feat(\"B\")) // ...and also drops feature B\n  // releases 4 and 5 were bug fixes, and no features were added or removed\n  .addOne(intervalFrom(6) -\u003e Feat(\"D\")) // release 6 adds feature D...\n  .removeOne(intervalFrom(6) -\u003e Feat(\"A\")) // ...and also drops feature A\n\nprint(\"The features in release 4: \")\nprintln(multi(4).mkString(\", \"))\nprint(\"The unique feature groups in all releases (probably not very useful): \")\nprintln(multi.values.mkString(\"; \"))\nprint(\"The unique features in all releases (probably more useful): \")\nprintln(multi.valuesOne.mkString(\", \"))\nprint(\"The releases where feature \\\"A\\\" existed: \")\nprintln(multi.intervalsOne(Feat(\"A\")).mkString(\", \"))\nprint(\"The releases where feature \\\"C\\\" existed: \")\nprintln(multi.intervalsOne(Feat(\"C\")).mkString(\", \"))\nprintln(\"The feature combinations in all release intervals:\")\nprintln(multi)\n```\n\nThe results show the various ways to query the structure, including (in the last one) the unique feature combinations in\neach release-based interval. (Note that releases 3, 4, and 5 share the same feature set, i.e., the combination of \nfeatures A and C is \"valid\" in the [3..5] release interval.)\n\n```text\nThe features in release 4: Feat(A), Feat(C)\nThe unique feature groups in all releases (probably not very useful): Set(Feat(A), Feat(B)); Set(Feat(A), Feat(C)); Set(Feat(A)); Set(Feat(C), Feat(D))\nThe unique features in all releases (probably more useful): Feat(A), Feat(B), Feat(C), Feat(D)\nThe releases where feature \"A\" existed: [1..5]\nThe releases where feature \"C\" existed: [3..+∞)\nThe feature combinations in all release intervals:\n| 1 .. 1            | 2 .. 2            | 3 .. 5            | 6 .. +∞           |\n| {Feat(A)}         |\n                    | {Feat(A),Feat(B)} |\n                                        | {Feat(A),Feat(C)} |\n                                                            | {Feat(C),Feat(D)} |\n```\n\nFor the same reasons explained earlier, one might want to use `DataMulti` with two dimensions instead of one to model\nwhat features belong in what releases (integer-based release dimension) as well as when the product management decisions\nwere made to include/exclude features in each release (temporal-based knowledge dimension). These ideas are explored\nfurther in the `SoftwareProductFeatureManagement` example.\n\nAnother example might be representing [piecewise functions](https://en.wikipedia.org/wiki/Piecewise_function) coherently\nby using a function type as the value where different function pieces are valid in different domain intervals. The reLU\n([rectified linear unit](https://en.wikipedia.org/wiki/Rectifier_(neural_networks))) function is a simple piecewise\nfunction which, given some `x`, returns `x` when `x` is greater than zero and zero otherwise. This can be expressed\ndirectly using intervalidus to manage the function pieces as follows:\n\n```scala 3\nimport intervalidus.ContinuousValue.given\nimport intervalidus.Interval1D.*\nimport intervalidus.immutable.Data\n\nval reLU = new (Double =\u003e Double):\n  private val pieceAt = Data[Double =\u003e Double, Domain.In1D[Double]]()\n    .set(intervalFrom(0.0) -\u003e identity)\n    .set(intervalToBefore(0.0) -\u003e (_ =\u003e 0.0))\n  override def apply(d: Double): Double = pieceAt(d).apply(d)\n\nprintln(s\"reLU at -1 = ${reLU(-1)}\") // reLU at -1 = 0\nprintln(s\"reLU at  0 = ${reLU( 0)}\") // reLU at  0 = 0\nprintln(s\"reLU at  1 = ${reLU( 1)}\") // reLU at  1 = 1\n```\n\nAnother application is a simple variable, i.e., having a value that varies in time. The twist here is that an underlying\n`Data`structure maintains all historical values assigned with nanosecond resolution. Both mutable and immutable forms of\nthis `Variable` structure are available. Here's an example using the mutable form:\n```scala 3\nimport intervalidus.mutable.Variable\n\nval start = java.time.Instant.now()\nval x = Variable(5.0)\nx.set(5.1)\nx.set(5.2)\nprintln(x.history)\nprintln(s\"current value:  ${x.get}\")\nprintln(s\"prior value:    ${x.getPrior}\")\nprintln(s\"starting value: ${x.getAt(start)}\")\nprintln(s\"all values:     ${x.history.values}\")\n```\nThis prints:\n```text\n| -∞ .. 2025-11-18T17:57:36.754972399Z                             | 2025-11-18T17:57:36.754972400Z .. 2025-11-18T17:57:36.813380099Z | 2025-11-18T17:57:36.813380100Z .. +∞                             |\n| 5.0                                                              |\n                                                                   | 5.1                                                              |\n                                                                                                                                      | 5.2                                                              |\n\ncurrent value:  5.2\nprior value:    Some(5.1)\nstarting value: 5.0\nall values:     Set(5.2, 5.1, 5.0)\n```\n\n---\n\nThe same methods are available in both mutable/immutable variants (though parameter and\nreturn types vary). See the [full API documentation](https://rremple.github.io/intervalidus/api/intervalidus) for details on\nthese methods.\n\nThese query methods provide various data, difference, and Boolean results:\n\n- `get` / `getOption` / `getAt` / `getDataAt` / `getAll` / `getIntersecting`\n- `intersects`\n- `foldLeft`\n- `isEmpty` / `size`\n- `domain` / `domainComplement`\n- `values` / `intervals` / `allIntervals`\n- `diffActionsFrom`\n\nThese methods return a new structure:\n\n- `copy` / `toImmutable` / `toMutable`\n- `zip` / `zipAll`\n- `getByDimension` / `getByHeadDimension`\n\nThese mutation methods return a new structure when using immutable and `Unit` when using mutable:\n\n- `remove` (`-`) / `removeMany` (`--`) / `removeValue`\n- `replace` / `replaceByKey` / `update` / `merge`\n- `set` (`+`) / `setMany` (`++`) / `setIfNoConflict` / `fill`\n- `compress` / `compressAll` / `recompressAll`\n- `filter`\n- `map` / `mapValues` / `mapIntervals` / `collect` / `flatMap` (the immutable variant allows altering type parameters)\n- `applyDiffActions` / `syncWith`\n\n## Using and Extending\n\nThere is nothing remarkable about `LocalDate` and the other types used in the examples above. These are examples of\nsomething called \"domain values\" over which a\n\"domain\" can be defined. Domain values are finite with min/max values, an order, and an ordered hash function. There are\ntwo kinds of domain values: discrete and continuous. Discrete values have methods for finding successors/predecessors\nwhere continuous values do not. It is the domain built on top of the domain value that is used to define \"interval\"\nboundaries. Intervals using continuous domain values have boundaries that are either open or closed where those using\ndiscrete domain boundaries (apart from -∞ and +∞) are always closed. Note that domains and\nintervals with more than one dimension may include both discrete and continuous dimensions, as was shown in an example\nearlier.\n\nDiscrete and continuous domain values have different notions of adjacency.\n\n- Two elements of a domain over a set of discrete values are adjacent only if one of the elements is the successor (or\n  predecessor) of the other. For example, in a domain over discrete integers, `domain(1)` is adjacent to `domain(2)`\n  because there are no domain elements between `1` and `2`. The general notions of `leftAdjacent` (i.e., before) and\n  `rightAdjacent` (i.e., after) are based on the `successorOf` and `predecessorOf` methods of the discrete value type\n  class respectively.\n\n- Two elements of a domain over a set of continuous values are adjacent only if the two elements have the same value and\n  one is open and the other is closed. For example, in a domain over continuous doubles, `domain(1.0)` is adjacent to\n  `open(1.0)` because they share the same value `1.0` where one is closed and the other is open. The general notions of\n  `leftAdjacent` (i.e., before)\n  and `rightAdjacent` (i.e., after) yield the same result for a point: open if closed, and closed if open.\n\nA discrete value is a [type class](https://docs.scala-lang.org/scala3/book/ca-type-classes.html),\nand there are implementations given for the following types:\n\n- `Int`\n- `Long`\n- `LocalDate`\n- `BigInteger`\n\nA continuous value is also a type class, and there are implementations given for the following types:\n\n- `Double`\n- `LocalDateTime`\n- `Int`\n- `Long`\n- `LocalDate`\n\nBut if you have your own type with these properties, you can certainly give a `DiscreteValue` or `ContinuousValue` type\nclass definition for that type and use it in the definition of intervals, etc. To motivate\nan example where this makes sense, first consider a structure that represents the different colors of visible light as \nintervals of integer-valued nanometer wavelengths.\n\n```scala 3\nimport intervalidus.DiscreteValue.given\nimport intervalidus.Interval1D.*\nimport intervalidus.immutable.Data\n\nenum Color:\n  case Violet, Indigo, Blue, Green, Yellow, Orange, Red\n\nval colorByWavelength = Data[Color, Domain.In1D[Int]]()\n  .set(intervalFrom(380) -\u003e Color.Violet)\n  .set(intervalFrom(450) -\u003e Color.Blue)\n  .set(intervalFrom(495) -\u003e Color.Green)\n  .set(intervalFrom(570) -\u003e Color.Yellow)\n  .set(intervalFrom(590) -\u003e Color.Orange)\n  .set(intervalFrom(620) -\u003e Color.Red)\n  .remove(intervalFrom(750))\n\nprintln(colorByWavelength)\n```\nThis prints the following (rather sad) representation of a rainbow.\n```text\n| 380 .. 449 | 450 .. 494 | 495 .. 569 | 570 .. 589 | 590 .. 619 | 620 .. 749 |\n| Violet     |\n             | Blue       |\n                          | Green      |\n                                       | Yellow     |\n                                                    | Orange     |\n                                                                 | Red        |\n```\nAs shown earlier, the structure can be interrogated to get the color of a specific integer wavelength.\n```scala 3\nprintln(colorByWavelength(480)) // prints \"Blue\"\n```\nSay we also want to track our thoughts about colors. We could make use of a similar structure that associates thoughts\nwith these same integer-valued wavelength intervals like so.\n```scala 3\nval thoughtsByWavelength = Data[String, Domain.In1D[Int]]()\n  .set(intervalFrom(380).toBefore(570) -\u003e \"I like violet, blue, and green\")\n  .set(intervalFrom(620).to(750) -\u003e \"I like this too\")\nprintln(thoughtsByWavelength)\n```\nAlthough this does track the information we need to track, it seems burdensome to have to know the wavelengths of the\nnoted colors, and too fine-grained. Also, unless we include them in the noted thoughts, we no longer see any color \nnames in the output, only wavelengths, which makes things a bit too cryptic.\n```text\n| 380 .. 569                     | 570 .. 619                     | 620 .. 750                     |\n| I like violet, blue, and green |                                | I like this too                |\n```\n\nA more elegant approach would be to use intervals based on the colors themselves rather than their associated\nwavelengths. All we have to do is give a `DiscreteValue` type class definition for `Color` and then we can use it as the\nbasis for discrete intervals.\n\n```scala 3\ngiven DiscreteValue[Color] with\n  override val minValue: Color = Color.values.head // Violet\n  override val maxValue: Color = Color.values.last // Red\n\n  override def predecessorOf(x: Color): Option[Color] =\n    if x == minValue then None else Some(Color.fromOrdinal(x.ordinal - 1))\n  override def successorOf(x: Color): Option[Color] =\n    if x == maxValue then None else Some(Color.fromOrdinal(x.ordinal + 1))\n\n  override def compare(lhs: Color, rhs: Color): Int = lhs.ordinal.compareTo(rhs.ordinal)\n  override def orderedHashOf(x: Color): Double = x.ordinal\n\nval thoughtsByColor = Data[String, Domain.In1D[Color]]()\n  .set(intervalFrom(Color.Violet).to(Color.Green) -\u003e \"I like violet, blue, and green\")\n  .set(intervalAt(Color.Red) -\u003e \"I like this too\")\nprintln(thoughtsByColor)\n```\nThis associates our thoughts with color-based intervals rather than wavelength-based intervals, with the following\noutput.\n```text\n| Violet .. Green                | Yellow .. Orange               | Red .. Red                     |\n| I like violet, blue, and green |\n                                                                  | I like this too                |\n```\nAnd this structure can be interrogated to get the thought for a specific color without knowing anything about wavelengths.\n```scala 3\nprintln(thoughtsByColor(Color.Blue)) // prints \"I like violet, blue, and green\"\n```\n\nBecause creating a `DiscreteValue` type class definition based on a finite sequence of unique items is a common need, a\nhelper method `fromSeq` is provided that reduces boilerplate code. The following code is equivalent to what is shown \nabove.\n\n```scala 3\nenum Color:\n  case Violet, Indigo, Blue, Green, Yellow, Orange, Red\n\ngiven DiscreteValue[Color] = DiscreteValue.fromSeq(Color.values.toIndexedSeq)\n\nval thoughtsByColor = Data[String, Domain.In1D[Color]]() //...\n```\n\nOr, even briefer, most enums can have the discrete value \n[type class derived automatically](https://docs.scala-lang.org/scala3/reference/contextual/derivation.html)\n(some restrictions apply, \ne.g., the enum cannot be declared inside a function).\n```scala 3\nenum Color derives DiscreteValue:\n  case Violet, Indigo, Blue, Green, Yellow, Orange, Red\n\nval thoughtsByColor = Data[String, Domain.In1D[Color]]() //...\n```\n\nYou can extend through composition. One example is `Variable`, described above. Another is `DataVersioned`, which mimics\nthe `Data` API but uses an underlying `Data`\nstructure of a higher dimension (i.e., with an additional integer head dimension) to create a versioned data structure.\nThe \"current\" version is tracked as internal state and methods accept version selection as a\n[context parameter](https://docs.scala-lang.org/scala3/book/ca-context-parameters.html), with \"current\" as the\ndefault version selection applied. Also, a notion of approval is supported by specifying a specific future version for\nanything unapproved, and a timestamped history of versions is maintained.\n\nYou can also extend through object-oriented inheritance. For example `DataMulti` extends the underlying class hierarchy\nof normal `Data` structures to provide multimap-like\ncapabilities. The inherited components store and operate on sets of values rather than individual values, which allows\nmultiple values to be valid in the same interval. When queried, values are returned as sets. There are also `addOne` /\n`addOneMany` and `removeOne` / `removeOneMany` methods which allow mutation of individual values across intervals,\n`valuesOne` and `intervalsOne` methods for querying by individual values rather than value sets, and a `mergeOne` method\nfor concatenating two structures (a `merge` that preserves individual value validity from both of the structures).\n\n## Software Structure\n\nBelow is the class diagram for the core bits of Intervalidus:\n\n![core class diagram](/doc/intervalidus-core.svg)\n\nIntervalidus supports data in an arbitrary number of dimensions by leveraging Scala 3's\n[generic programming with tuples](https://www.scala-lang.org/2021/02/26/tuples-bring-generic-programming-to-scala-3.html). \nThe only dimension-specific classes are the base 1D cases of domains and intervals. A more generic notion of a\n`DomainLike` tuple underpins multidimensional support across all Intervalidus data structures. You may never need more \nthan three or four dimensions, but if you do, the support is available to you (but remember: flatter is faster).\n\nFurthermore, the definitions and implementations of methods across mutable/immutable variants of `Data` and `DataMulti` \nhave been made as generic as possible to avoid repetitive code/scaladoc (DRY). However, this can make it\nharder to navigate to methods. (Although this was a larger issue back when Intervalidus had separate class hierarchies\nfor each dimension supported.) The following (rather unorthodox) diagram shows where to find each method in a\nkind of Venn-like way, where overlaps indicate a definition (and documentation) is in the lower trait with the\nimplementation in the higher, inheriting trait/class:\n\n![core trait stack diagram](/doc/intervalidus-trait-stack.svg)\n\n## Internals and extras\n\nBoth the mutable and immutable variants of `Data`, `DataMulti`, and `DataVersioned` use three mutable data\nstructures internally for managing state, two of which are custom (in the `collection` subproject):\n\n- A mutable standard library `TreeMap`, ordered by the start of each interval. This allows for fast in-order retrieval\n  in methods like `getAll`, and is essential for deterministic results in methods like `foldLeft` that use `getAll`.\n\n- A mutable multimap that associates each value with all the intervals in which that value is valid, ordered by the\n  start of each interval. This speeds up operations like `compress`, which improves performance for all mutation\n  operations that use `compress`. Intervalidus uses its own compact implementation of a `MultiMapSorted` (based on\n  standard library `Map` and `SortedSet`) which is somewhat similar to `SortedMultiDict` in `scala-collection-contrib`,\n  but returns the _values_ in order, not the _keys_. Having values in order is essential for deterministic results from\n  `compress` and all mutation operations relying on `compress`. Only the mutable variant is used internally, but an\n  immutable variant is also provided. Note that the sample billing application uses this multimap directly for managing\n  customer transactions.\n\n- A \"box search tree\", which is a hyperoctree (i.e., a B-tree, quadtree, octree, etc., depending on the dimension) that\n  supports quick\n  retrieval by interval. Box search trees manage \"boxed\" data in multidimensional double space. Unlike classic\n  spacial search trees (used in collision detection and the like), these data structures manage \"boxes\" rather than\n  individual points. Boxes are split and stored in all applicable subtrees (hyperoctants) of the data structure as \n  subtrees are split. Intervalildus uses the ordered hashes of interval boundaries to approximate all\n  domain intervals as boxes in double space, and then manages valid data associated with these boxes in the box\n  search tree. This not only results in dramatically faster retrieval (e.g., `getAt` and `getIntersecting`), since many\n  mutation operations use intersection retrieval in their own logic, they are made dramatically faster as well. Only the\n  mutable variant is used internally, but an immutable variant is also provided.\n\nAlthough the custom multimap and box search tree data structures were built for internal use, they may be useful outside\nthe Intervalidus context. As described above, there are both immutable and mutable variants of each data structure, as\nshown in the following diagram:\n\n![box tree and multimap diagram](/doc/intervalidus-box-tree-multimap.svg)\n\nAgain, looking at method definition/implementation in a Venn-like way, here's the full API for a `BoxTree` and related\nobjects:\n\n![box tree trait stack diagram](/doc/intervalidus-trait-stack-box-tree.svg)\n\nNote that box search trees are tunable via environment variables.\n\n- The default capacity of leaf nodes is 256, which was found to be optimal in micro-benchmarks. This can be overridden\n  by setting the environment variable `INTERVALIDUS_TREE_NODE_CAPACITY`.\n\n- The default depth limit of trees is 32, which was found to be optimal in micro-benchmarks. This can be overridden by\n  setting the environment variable `INTERVALIDUS_TREE_DEPTH_LIMIT`.\n\nApart from `collection`, there are a few other subprojects that are worth mentioning:\n\n- As described earlier, in the `intervalidus-examples` subproject there is a sample billing application that shows how\n  Intervalidus structures can be used to support time-oriented logic like billing directly.\n\n- Also in the `intervalidus-examples` subproject there is a sample \"Software Product Feature Lifecycle and Rollout\n  Management\" application. It shows how Intervalidus structures, including the unique features of `DataMulti` and\n  `DataVersioned`, could be used to manage and track the evolution of a software product.\n\n- Also in the `intervalidus-examples` subproject there is a sample rules-based application that shows how Intervalidus\n  structures can be used to capture the outcomes of patients in a clinical trial. It uses a toy rules engine to\n  interpret daily measurements, and Intervalidus to capture progress concisely.\n\n- The above example leverages a toy rules engine that is available in a separate subproject: `intervalidus-tinyrule`.\n  (This is not strictly related to Intervalidus, so it is tucked under a directory called `sidequests`.) It uses some\n  interesting generic and metaprogramming features of Scala 3 to represent and process facts.\n\n- There is a separate `bench` subproject that leverages the Java Microbenchmark Harness (jmh) framework to benchmark\n  methods, including relative performance of experimental vs. non-experimental features.\n\n- There are sample JSON pickling subprojects `intervalidus-upickle` and `intervalidus-weepickle` which could be useful\n  when managing Intervalidus data in a JSON data store (e.g., MongoDB) and/or serializing data through web services.\n  (There are many JSON frameworks, and one could use these subprojects as a starting point to add support for others.)\n\n- The `intervalidus-example-mongodb` subproject leverages these pickling subprojects to demonstrate how dimensional data\n  can be managed in a database. It uses MongoDB (via Testcontainers) to store, retrieve, and update data.\n\nLastly, there is a context parameter component used to enable/disable experimental features (a.k.a., feature flagging)\ncalled `Experimental`. The default implementation given disables all experimental features. But one can enable something\nexperimental simply by giving an alternative implementation of `Experimental` when the structure is constructed.\n\nE.g., consider the requirement for intervals to be disjoint (i.e., non-overlapping) in all valid data. All mutation\noperations (e.g. `set`) maintain this invariant automatically, but one could still construct a structure incorrectly\nwith data that are not disjoint to start (which will cause Intervalidus to be weirdly unpredictable). Or maybe\nIntervalidus has a bug, and some mutation operation isn't maintaining the invariant. Although constantly checking for\ndisjointedness is a huge performance burden -- especially for immutable structures and/or higher-dimensional\nstructures -- it may be worth doing during initial development and testing. So there is an experimental feature called\n**\"requireDisjoint\"** that, if set, will validate the disjointedness of all data with every construction.\n\n```scala 3\nimport intervalidus.DiscreteValue.given\nimport intervalidus.Interval1D.*\nimport intervalidus.immutable.Data\nimport java.time.LocalDate.of as date\n\ngiven Experimental = Experimental(\"requireDisjoint\")\n\nval plan1d = Data(\n  Seq(\n    intervalFrom(date(2024, 4, 1)) -\u003e \"Premium\",\n    intervalFrom(date(2024, 1, 1)) -\u003e \"Basic\" // \u003c-- wrong, throws an IllegalArgumentException: requirement failed: data must be disjoint\n    // interval(date(2024, 1, 1), date(2024, 3, 31)) -\u003e \"Basic\" // \u003c-- right, does not throw\n  )\n)\n```\n\nMany experimental features such as **\"noSearchTree\"** and **\"noBruteForceUpdate\"** have come and gone. \nExisting experimental features that can be toggled are:\n\n- **\"requireDisjoint\"** This is described above.\n\n- **\"printExperimental\"** This feature simply prints a line when an experimental feature is/isn't being used. Useful if\n  there is uncertainty around if the context parameter is being set and passed along correctly in the correct scope.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frremple%2Fintervalidus","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frremple%2Fintervalidus","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frremple%2Fintervalidus/lists"}