{"id":19731358,"url":"https://github.com/cargosense/compare_chain","last_synced_at":"2025-04-07T17:10:39.976Z","repository":{"id":65547976,"uuid":"561531604","full_name":"CargoSense/compare_chain","owner":"CargoSense","description":"Chained semantic comparisons for Elixir.","archived":false,"fork":false,"pushed_at":"2025-03-01T15:06:37.000Z","size":120,"stargazers_count":37,"open_issues_count":1,"forks_count":0,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-03-31T15:19:32.205Z","etag":null,"topics":["elixir"],"latest_commit_sha":null,"homepage":"https://hex.pm/packages/compare_chain","language":"Elixir","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/CargoSense.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-11-03T22:31:29.000Z","updated_at":"2025-03-27T22:31:43.000Z","dependencies_parsed_at":"2025-01-16T01:09:09.495Z","dependency_job_id":"2ec22472-d305-44ed-9589-996b1ca6f5ef","html_url":"https://github.com/CargoSense/compare_chain","commit_stats":{"total_commits":84,"total_committers":5,"mean_commits":16.8,"dds":0.3571428571428571,"last_synced_commit":"4358007f2e74a706705b025fe384a52f15af955c"},"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CargoSense%2Fcompare_chain","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CargoSense%2Fcompare_chain/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CargoSense%2Fcompare_chain/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CargoSense%2Fcompare_chain/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/CargoSense","download_url":"https://codeload.github.com/CargoSense/compare_chain/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247694876,"owners_count":20980733,"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":["elixir"],"created_at":"2024-11-12T00:20:34.372Z","updated_at":"2025-04-07T17:10:39.946Z","avatar_url":"https://github.com/CargoSense.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# CompareChain\n\n**Chained semantic comparisons for Elixir.**\n\n[![Package](https://img.shields.io/hexpm/v/compare_chain?logo=elixir\u0026style=for-the-badge)](https://hex.pm/packages/compare_chain)\n[![Downloads](https://img.shields.io/hexpm/dt/compare_chain?logo=elixir\u0026style=for-the-badge)](https://hex.pm/packages/compare_chain)\n[![Build](https://img.shields.io/github/actions/workflow/status/CargoSense/compare_chain/ci.yml?branch=main\u0026logo=github\u0026style=for-the-badge)](https://github.com/CargoSense/compare_chain/actions/workflows/ci.yml)\n\n## Key Features\n\nCompareChain provides convenience macros for:\n\n- chained comparisons (`a \u003c b \u003c c`)\n- semantic comparisons using structural operators (`\u003c`, `\u003e`, `\u003c=`, `\u003e=`, `==`, `!=`, `===`, and `!==`)\n- combinations (`and`, `or`, and `not`)\n\n## Installation\n\nAdd `compare_chain` to your project's dependencies in `mix.exs` and run `mix deps.get`:\n\n```elixir\ndef deps do\n  [\n    {:compare_chain, \"~\u003e 0.5\"}\n  ]\nend\n```\n\n## Usage\n\nImport CompareChain to enable access to `CompareChain.compare?/1` and `CompareChain.compare?/2`:\n\n```elixir\niex\u003e import CompareChain\n\n# Chained comparisons\niex\u003e compare?(1 \u003c 2 \u003c 3)\ntrue\n\n# Semantic comparisons\niex\u003e compare?(~D[2017-03-31] \u003c ~D[2017-04-01], Date)\ntrue\n\n# Chained semantic comparisons\niex\u003e compare?(~D[2017-03-31] \u003c ~D[2017-04-01] \u003c ~D[2017-04-02], Date)\ntrue\n\n# Semantic comparisons with logical operators\niex\u003e compare?(~T[16:00:00] \u003c= ~T[16:00:00] and not (~T[17:00:00] \u003c= ~T[17:00:00]), Time)\nfalse\n\n# Complex expressions\niex\u003e compare?(%{a: ~T[16:00:00]}.a \u003c= ~T[17:00:00], Time)\ntrue\n```\n\nSee [CompareChain on HexDocs](https://hexdocs.pm/compare_chain) for more.\n\n## Background\n\nSay you have an interval of time bounded by two `%Date{}` structs, `start_date` and `end_date`, and you want to know whether or not a third `date` falls within that interval. How would you write this in Elixir?\n\n```elixir\nDate.compare(start_date, date) == :lt and\n  Date.compare(date, end_date) == :lt\n```\n\nThe above code is verbose, somewhat hard to read, and potentially incorrect (though not obviously so). What if `date` is considered \"within\" the interval inclusive of the `start_date` or `end_date`? To include the bounds in the comparison, you'd instead write the expression like this:\n\n```elixir\nDate.compare(start_date, date) != :gt and\n  Date.compare(date, end_date) != :gt\n\n# …or, even more verbosely:\nDate.compare(start_date, date) in [:lt, :eq] and\n  Date.compare(date, end_date) in [:lt, :eq]\n```\n\nTo spot the difference between these two cases, you must keep in mind:\n\n- the order of the arguments passed to `Date.compare/2`,\n- the specific comparison operators for each clause (`==` vs. `!=`), and\n- the specific comparison atoms for each clause (`:lt` vs. `:gt`).\n\nContrast this example with equivalent Python code:\n\n```python\n# excluding bounds\nstart_date \u003c date \u003c end_date\n\n# including bounds\nstart_date \u003c= date \u003c= end_date\n```\n\nMuch easier to read! Why can't you write this in Elixir? Two reasons:\n\n1. Structural comparison operators\n2. Chained vs. nested comparisons\n\n### Structural Comparison Operators\n\nOperators like `\u003c` do _structural_ comparison (instead of _semantic_ comparison). From the [`Kernel` docs](https://hexdocs.pm/elixir/Kernel.html#module-structural-comparison):\n\n\u003e …**comparisons in Elixir are structural**, as it has the goal of comparing data types as efficiently as possible to create flexible and performant data structures. This distinction is specially important for functions that provide ordering, such as `\u003e/2`, `\u003c/2`, `\u003e=/2`, `\u003c=/2`, `min/2`, and `max/2`. For example:\n\u003e\n\u003e ```elixir\n\u003e ~D[2017-03-31] \u003e ~D[2017-04-01]\n\u003e ```\n\u003e\n\u003e will return `true` because structural comparison compares the `:day` field before `:month` or `:year`. In order to perform semantic comparisons, the relevant data-types provide a `compare/2` function, such as `Date.compare/2`:\n\u003e\n\u003e ```elixir\n\u003e iex\u003e Date.compare(~D[2017-03-31], ~D[2017-04-01])\n\u003e :lt\n\u003e ```\n\nIn other words, although `~D[2017-03-31] \u003e ~D[2017-04-01]` is valid code, it does _not_ tell you if `~D[2017-03-31]` is a later date than `~D[2017-04-01]` as you might expect.\nInstead, you'd use `Date.compare/2`.\n\n### Chained vs. Nested Comparisons\n\nAdditionally, even if `~D[2017-03-31] \u003e ~D[2017-04-01]` did semantic comparison, you still couldn't write the interval check like you do in Python. In Python, an expression like `1 \u003c 2 \u003c 3` is syntactic sugar for `(1 \u003c 2) and (2 \u003c 3)` (a series of \"chained\" expressions).\n\nElixir doesn't provide an equivalent syntactic sugar. Instead, `1 \u003c 2 \u003c 3` is evaluated as `(1 \u003c 2) \u003c 3` (a series of \"nested\" expressions). `(1 \u003c 2) \u003c 3` evaluates to `true \u003c 3` which is _probably_ not what you want!\n\n### A Solution!\n\nCompareChain addresses these complexities with the macro `CompareChain.compare?/2`:\n\n```elixir\nimport CompareChain\n\n# excluding bounds\ncompare?(start_date \u003c date \u003c end_date, Date)\n\n# including bounds\ncompare?(start_date \u003c= date \u003c= end_date, Date)\n```\n\n`CompareChain.compare?/2` rewrites these expressions as:\n\n```elixir\n# excluding bounds\nDate.compare(start_date, date) == :lt and\n  Date.compare(date, end_date) == :lt\n\n# including bounds\nDate.compare(start_date, date) != :gt and\n  Date.compare(date, end_date) != :gt\n```\n\nYour code is more readable while remaining correct!\n\n`CompareChain.compare?/1` also enables chained comparison using the structural operators:\n\n```elixir\ncompare?(1 \u003c 2 \u003c 3)\n```\n\n## Acknowledgements\n\nThanks to [Ben Wilson](https://github.com/benwilson512) and [Michael Crumm](https://github.com/mcrumm) for the helpful discussions and their guidance!\n\nThanks as well to the folks who participated in the [elixir-lang-core](https://groups.google.com/g/elixir-lang-core) discussion, particularly Cliff whose [idea](https://groups.google.com/g/elixir-lang-core/c/W2TeQm5r1H4/m/ctVuN_woBgAJ) I shamelessly built off.\n\n## License\n\nCompareChain is freely available under the [MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcargosense%2Fcompare_chain","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcargosense%2Fcompare_chain","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcargosense%2Fcompare_chain/lists"}