├── .formatter.exs ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .tool-versions ├── .travis.yml ├── LICENSE ├── README.md ├── config └── config.exs ├── lib ├── ratio.ex └── ratio │ ├── coerce.ex │ ├── decimal_conversion.ex │ ├── float_conversion.ex │ └── numbers.ex ├── mix.exs ├── mix.lock └── test ├── ratio ├── decimal_conversion_test.exs ├── float_conversion_test.exs └── numbers_test.exs ├── ratio_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: [ 3 | "{lib,test,config}/**/*.{ex,exs}", 4 | "*.exs" 5 | ], 6 | import_deps: [:stream_data] 7 | ] 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: mix 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: ex_doc 10 | versions: 11 | - 0.23.0 12 | - 0.24.0 13 | - 0.24.1 14 | - dependency-name: earmark 15 | versions: 16 | - 1.4.13 17 | - 1.4.14 18 | - dependency-name: numbers 19 | versions: 20 | - 5.2.3 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # create this in .github/workflows/ci.yml 2 | name: ci 3 | on: push 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} 9 | strategy: 10 | matrix: 11 | include: 12 | - elixir: 1.14.x 13 | otp: 25.x 14 | - elixir: 1.14.x 15 | otp: 24.x 16 | - elixir: 1.14.x 17 | otp: 23.x 18 | - elixir: 1.13.x 19 | otp: 24.x 20 | - elixir: 1.13.x 21 | otp: 23.x 22 | - elixir: 1.13.x 23 | otp: 22.x 24 | - elixir: 1.12.x 25 | otp: 24.x 26 | - elixir: 1.12.x 27 | otp: 23.x 28 | - elixir: 1.12.x 29 | otp: 22.x 30 | - elixir: 1.11.x 31 | otp: 23.x 32 | - elixir: 1.11.x 33 | otp: 22.x 34 | - elixir: 1.11.x 35 | otp: 21.x 36 | - elixir: 1.10.x 37 | otp: 22.x 38 | - elixir: 1.10.x 39 | otp: 21.x 40 | - elixir: 1.9.x 41 | otp: 22.x 42 | - elixir: 1.9.x 43 | otp: 21.x 44 | - elixir: 1.9.x 45 | otp: 20.x 46 | - elixir: 1.8.x 47 | otp: 22.x 48 | - elixir: 1.8.x 49 | otp: 21.x 50 | - elixir: 1.8.x 51 | otp: 20.x 52 | # - elixir: 1.7.x 53 | # otp: 22.x 54 | # - elixir: 1.7.x 55 | # otp: 21.x 56 | # - elixir: 1.7.x 57 | # otp: 20.x 58 | # - elixir: 1.7.x 59 | # otp: 19.x 60 | # - elixir: 1.6.x 61 | # otp: 20.x 62 | # - elixir: 1.6.x 63 | # otp: 19.x 64 | env: 65 | MIX_ENV: test 66 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 67 | # NODE_COVERALLS_DEBUG: 1 68 | steps: 69 | - uses: actions/checkout@v2 70 | - uses: erlef/setup-beam@v1 71 | with: 72 | otp-version: ${{matrix.otp}} 73 | elixir-version: ${{matrix.elixir}} 74 | - uses: actions/cache@v2 75 | id: mix-cache 76 | with: 77 | path: | 78 | deps 79 | _build 80 | key: ${{ runner.os }}-${{ matrix.otp}}-${{ matrix.elixir }}-mix-${{ hashFiles('**/mix.lock') }} 81 | restore-keys: | 82 | ${{ runner.os }}-${{ matrix.otp}}-${{ matrix.elixir }}-mix- 83 | - if: steps.mix-cache.outputs.cache-hit != 'true' 84 | run: | 85 | mix deps.get 86 | mix deps.compile 87 | - run: mix compile 88 | - run: mix test # Testing is already done as part of the next task: 89 | # - run: mix coveralls.github --trace 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | /doc 5 | erl_crash.dump 6 | *.ez 7 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.16.2-otp-26 2 | erlang 26.2.3 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 3 | - 1.6.0 4 | - 1.6.1 5 | - 1.6.2 6 | - 1.6.3 7 | - 1.6.4 8 | - 1.6.5 9 | - 1.6.6 10 | - 1.7.0 11 | - 1.7.1 12 | - 1.7.2 13 | - 1.8 14 | - 1.9 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Qqwy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ratio 2 | 3 | 4 | [![hex.pm version](https://img.shields.io/hexpm/v/ratio.svg)](https://hex.pm/packages/ratio) 5 | [![Documentation](https://img.shields.io/badge/hexdocs-latest-blue.svg)](https://hexdocs.pm/ratio/index.html) 6 | [![ci](https://github.com/Qqwy/elixir-rational/actions/workflows/ci.yml/badge.svg)](https://github.com/Qqwy/elixir-rational/actions/workflows/ci.yml) 7 | 8 | This library allows you to use Rational numbers in Elixir, to enable exact calculations with all numbers big and small. 9 | 10 | Ratio follows the Numeric behaviour from [Numbers](https://github.com/Qqwy/elixir_number), and can therefore be used in combination with any data type that uses Numbers (such as [Tensor](https://hex.pm/packages/tensor) and [ComplexNum](https://github.com/Qqwy/elixir_complex_num)). 11 | 12 | ## Using Ratio 13 | 14 | `Ratio` defines arithmetic and comparison operations to work with rational numbers. 15 | 16 | Rational numbers can be created by using `Ratio.new/2`, 17 | or by calling mathematical operators where one of the two operands is already a rational number. 18 | 19 | 20 | ### Shorthand infix construction operator 21 | 22 | Since version 4.0, `Ratio` no longer defines an infix operator to create rational numbers. 23 | Instead, rational numbers are made using `Ratio.new`, 24 | and as the output from using an existing `Ratio` struct with a mathematical operation. 25 | 26 | If you do want to use an infix operator such as 27 | `<~>` (supported in all Elixir versions) 28 | or `<|>` (deprecated in Elixir v1.14, the default of older versions of the `Ratio` library) 29 | 30 | you can add the following one-liner to the module(s) in which you want to use it: 31 | 32 | ```elixir 33 | defdelegate numerator <~> denominator, to: Ratio, as: :new 34 | ``` 35 | 36 | ### Basic functionality 37 | 38 | Rational numbers can be manipulated using the functions in the [`Ratio`](https://hexdocs.pm/ratio/Ratio.html) module. 39 | 40 | ```elixir 41 | iex> Ratio.mult(Ratio.new(1, 3), Ratio.new(1, 2)) 42 | Ratio.new(1, 6) 43 | iex> Ratio.div(Ratio.new(2, 3), Ratio.new(8, 5)) 44 | Ratio.new(5, 12) 45 | iex> Ratio.pow(Ratio.new(2), 4) 46 | Ratio.new(16, 1) 47 | ``` 48 | 49 | The Ratio module also contains: 50 | - a guard-safe `is_rational/1` check. 51 | - a `compare/2` function for use with e.g. `Enum.sort`. 52 | - `to_float/1` to (lossly) convert a rational into a float. 53 | 54 | ### Inline Math Operators and Casting 55 | 56 | Ratio interopts with the [`Numbers`](https://github.com/Qqwy/elixir-number) library: 57 | If you want to overload Elixir's builtin math operators, 58 | you can add `use Numbers, overload_operators: true` to your module. 59 | 60 | This also allows you to pass in a rational number as one argument 61 | and an integer, float or Decimal (if you have installed the `Decimal` library), 62 | which are then cast to rational numbers whenever necessary: 63 | 64 | ``` elixir 65 | defmodule IDoAlotOfMathHere do 66 | defdelegate numerator <~> denominator, to: Ratio, as: :new 67 | use Numbers, overload_operators: true 68 | 69 | def calculate(input) do 70 | num = input <~> 2 71 | result = num * 2 + (3 <~> 4) * 5.0 72 | result / 2 73 | end 74 | end 75 | 76 | ``` 77 | 78 | ``` elixir 79 | iex> IDoAlotOfMathHere.calculate(42) 80 | Ratio.new(183, 8) 81 | ``` 82 | 83 | 84 | ## Installation 85 | 86 | The package can be installed from hex, by adding `:ratio` to your list of dependencies in `mix.exs`: 87 | 88 | def deps do 89 | [ 90 | {:ratio, "~> 4.0"} 91 | ] 92 | end 93 | 94 | 95 | 96 | ## Changelog 97 | - 4.0.1 - 98 | - Fix compiler warnings on Elixir v1.16 (c.f. #128). Thank you, @kidq330 99 | - 4.0.0 - 100 | - Remove infix operator `<|>` as its usage is deprecated in Elixir v1.14. This is a backwards-incompatible change. If you want to use the old syntax with the new version, add `defdelegate num <|> denom, to: Ratio, as: :new` to your module. Alternatively, you might want to use the not-deprecated `<~>` operator for this instead. 101 | - Switch the `Inspect` implementation to use the form `Ratio.new(10, 20)` instead of `10 <|> 20`, related to above. This is also a backwards-incompatible change. 102 | - Remove implementation of `String.Chars`, as the earlier implementation was not a (non-programmer) human-readable format. 103 | - Ensure that the right-hand-side operand of calls to `Ratio.{add, sub, mult, div}/2` is allowed to be an integer for ease of use and backwards compatibility. Thank you for noticing this problem, @kipcole9 ! (c.f. #111) 104 | - 3.0.2 - 105 | - Fixes: A bug with `<|>` when the numerator was a rational and the denuminator an integer. (c.f. #104) Thank you, @varsill! 106 | - 3.0.1 - 107 | - Fixes: 108 | - Problem where `Ratio.ceil/1` would be off-by-one (c.f. #89). Thank you, @Hajto! 109 | - Problem where `Ratio.pow/2` would return an integer rather than a new Ratio.(c.f. #100). Thank you, @speeddragon! 110 | - 3.0.0 - 111 | - All operators except `<|>` are removed from Ratio. Instead, the operators defined by [`Numbers`](https://github.com/Qqwy/elixir-number) (which `Ratio` depends on) can be used, by adding `use Numbers, overload_operators: true` to your modules. (c.f. #34) 112 | - All math-based functions expect and return `Ratio` structs (rather than also working on integers and returning integers sometimes if the output turned out to be a whole number). (c.f. #43) 113 | This makes the code more efficient and more clear for users. 114 | - Ratio structs representing whole numbers are no longer implicitly converted 'back' to integers, as this behaviour was confusing. (c.f. #28) 115 | - If conversion to/from other number-like types is really desired, 116 | use the automatic conversions provided by `Ratio.new`, `<|>` 117 | or (a bit slower but more general) the math functions exposed by [`Numbers`](https://github.com/Qqwy/elixir-number). 118 | Ratio ships with implementations of `Coerce.defcoercion` for Integer -> Ratio, Float -> Ratio and Decimal -> Ratio. 119 | - `is_rational?/1` is replaced with the guard-safe `is_rational/1` (only exported on Erlang versions where `:erlang.map_get/2` is available, i.e. >= OTP 21.0.) (c.f. #37) 120 | - `Float.ratio/1` is now used to convert floats into `Ratio` structs, rather than maintaining a hand-written version of this logic. (c.f #46) Thank you, @marcinwasowicz ! 121 | - A lot of property-based tests have been added to get some level of confidence of the correctness of the library's operations. 122 | - 2.4.2 Uses `extra_applications` in `mix.exs` to silence warnings in Elixir 1.11 and onwards. 123 | - 2.4.1 Fixes a bug in the decimal conversion implementation where certain decimals were not converted properly. Thank you, @iterateNZ! 124 | - 2.4.0 Adds optional support for automatic conversion from [Decimal](https://github.com/ericmj/decimal)s. Thank you, @kipcole ! 125 | - 2.3.1 Removes spurious printing statement in `Rational.FloatConversion` that would output a line of text at compile-time. Fixes support for Numbers v5+ which was broken. 126 | - 2.3.0 Adds `trunc` and `to_floor_error` functions. 127 | - 2.1.1 Fixes implementation of `floor` and `ceil` which was counter-intuitive for negative numbers (it now correctly rounds towards negative infinity). 128 | - Drops support for Elixir versions older than 1.4, because of use of `Integer.floor_div`. 129 | - First version to support new Erlang versions (20 and onward) that have native `floor` and `ceil` functions. 130 | - 2.1.0 Adds optional overloaded comparison operators. 131 | - 2.0.0 Breaking change: Brought `Ratio.compare/2` in line with Elixir's comparison function guideline, to return `:lt | :eq | :gt`. (This used to be `-1 | 0 | 1`). 132 | - 1.2.9 Improved documentation. (Thanks, @morontt!) 133 | - 1.2.8 Adding `:numbers` to the `applications:` list, to ensure that no warnings are thrown when building releases on Elixir < 1.4.0. 134 | - 1.2.6, 1.2.7 Improving documentation. 135 | - 1.2.5 added `ceil/1` and `floor/1`. 136 | - 1.2.4 Fixes Elixir 1.4 warnings in the `mix.exs` file. 137 | - 1.2.3 Upgraded version of the `Numbers` dependency to 2.0. 138 | - 1.2.2 Added default argument to `Ratio.new/2`, to follow the Numeric behaviour fully, and added `Ratio.minus/1` as alias for `Ratio.negate/1` for the same reason. 139 | - 1.2.0 Changed name of `Ratio.mul/2` to `Ratio.mult/2`, to avoid ambiguety, and to allow incorporation with `Numbers`. Deprecation Warning was added to using `Ratio.mul/2`. 140 | - 1.1.1 Negative floats are now converted correctly. 141 | - 1.1.0 Elixir 1.3 compliance (Statefree if/else/catch clauses, etc.) 142 | - 1.0.0 Proper `__using__` macro, with more readable option names. Stable release. 143 | - 0.6.0 First public release 144 | - 0.0.1 First features 145 | 146 | 147 | ## Difference with the 'rational' library 148 | 149 | Observant readers might notice that there also is a '[rational](https://hex.pm/packages/rational)' library in Hex.pm. The design idea between that library vs. this one is a bit different: `Ratio` hides the internal data representation as much as possible, and numbers are therefore only created using `Ratio.new/2`. This has as mayor advantage that the internal representation is always correct and simplified. 150 | 151 | The Ratio library also (optionally) overrides (by virtue of the `Numbers` library) the built-in math operations `+, -, *, /, div, abs` so they work with combinations of integers, floats and rationals. 152 | 153 | Finally, Ratio follows the Numeric behaviour, which means that it can be used with any data types that follow [Numbers](https://github.com/Qqwy/elixir_number). 154 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # Ratio does not have any config 2 | -------------------------------------------------------------------------------- /lib/ratio.ex: -------------------------------------------------------------------------------- 1 | defmodule Ratio do 2 | @vsn "3.0.0" 3 | 4 | import Kernel, 5 | except: [ 6 | div: 2, 7 | abs: 1, 8 | floor: 1, 9 | ceil: 1, 10 | trunc: 1 11 | ] 12 | 13 | @moduledoc """ 14 | This module allows you to use Rational numbers in Elixir, to enable exact calculations with all numbers big and small. 15 | 16 | `Ratio` defines arithmetic and comparison operations to work with rational numbers. 17 | 18 | 19 | This module also contains: 20 | - a guard-safe `is_rational/1` check. 21 | - a `compare/2` function for use with e.g. `Enum.sort`. 22 | - `to_float/1` to (lossly) convert a rational into a float. 23 | 24 | # Shorthand infix construction operator 25 | 26 | Since version 4.0, `Ratio` no longer defines an infix operator to create rational numbers. 27 | Instead, rational numbers are made using `Ratio.new`, 28 | and as the output from using an existing `Ratio` struct with a mathematical operation. 29 | 30 | If you do want to use an infix operator such as 31 | `<~>` (supported in all Elixir versions) 32 | or `<|>` (deprecated in Elixir v1.14, the default of older versions of the `Ratio` library) 33 | 34 | you can add the following one-liner to the module(s) in which you want to use it: 35 | 36 | ```elixir 37 | defdelegate numerator <~> denominator, to: Ratio, as: :new 38 | ``` 39 | 40 | ## Inline Math Operators and Casting 41 | 42 | Ratio interopts with the `Numbers` library: 43 | If you want to overload Elixir's builtin math operators, you can use `use Numbers, overload_operators: true`. 44 | 45 | This also allows you to pass in a rational number as one argument 46 | and an integer, float or Decimal (if you have installed the `Decimal` library), 47 | which are then cast to rational numbers whenever necessary. 48 | 49 | ``` elixir 50 | defmodule IDoAlotOfMathHere do 51 | defdelegate numerator <~> denominator, to: Ratio, as: :new 52 | use Numbers, overload_operators: true 53 | 54 | def calculate(input) do 55 | num = input <~> 2 56 | result = num * 2 + (3 <~> 4) * 5.0 57 | result / 2 58 | end 59 | end 60 | ``` 61 | 62 | ``` 63 | iex> IDoAlotOfMathHere.calculate(42) 64 | Ratio.new(183, 8) 65 | ``` 66 | 67 | 68 | 69 | """ 70 | 71 | defmacro __using__(_opts) do 72 | raise """ 73 | Writing `use Ratio` (with or without options) is no longer possible since v3.0. 74 | 75 | Instead: 76 | 77 | - For basic usage, call functions of the `Ratio` module directly without `use` or `import`. 78 | - To add an infix operator for rational number creation, write `defdelegate numerator <|> denominator, to: Ratio, as: :new`. 79 | - To override the inline math operators, write `use Numbers, overload_operators: true`. (and see the `Numbers` module/library documentation for more information.) 80 | """ 81 | end 82 | 83 | @doc """ 84 | A Rational number is defined as a numerator and a denominator. 85 | Both the numerator and the denominator are integers. 86 | If you want to match for a rational number, you can do so by matching against this Struct. 87 | 88 | Note that *directly manipulating* the struct, however, is usually a bad idea, as then there are no validity checks, nor wil the rational be simplified. 89 | 90 | Use `Ratio.new/2` instead. 91 | """ 92 | defstruct numerator: 0, denominator: 1 93 | @type t :: %Ratio{numerator: integer(), denominator: pos_integer()} 94 | 95 | @doc """ 96 | Check to see whether something is a ratioal struct. 97 | 98 | On recent OTP versions that expose `:erlang.map_get/2` this function is guard safe. 99 | 100 | iex> require Ratio 101 | iex> Ratio.is_rational(Ratio.new(1, 2)) 102 | true 103 | iex> Ratio.is_rational(Ratio.new(10)) 104 | true 105 | iex> Ratio.is_rational(42) 106 | false 107 | iex> Ratio.is_rational(%{}) 108 | false 109 | iex> Ratio.is_rational("My quick brown fox") 110 | false 111 | """ 112 | if function_exported?(:erlang, :map_get, 2) and function_exported?(Kernel, :is_map_key, 2) do 113 | defguard is_rational(val) 114 | when is_map(val) and is_map_key(val, :__struct__) and is_struct(val) and 115 | :erlang.map_get(:__struct__, val) == __MODULE__ 116 | else 117 | def is_rational(val) 118 | def is_rational(%Ratio{}), do: true 119 | def is_rational(_), do: false 120 | end 121 | 122 | @doc """ 123 | Creates a new Rational number. 124 | This number is simplified to the most basic form automatically. 125 | 126 | Rational numbers with a `0` as denominator are not allowed. 127 | 128 | Note that it is recommended to use integer numbers for the numerator and the denominator. 129 | 130 | ## Floats 131 | 132 | *If possible, don't use them.* 133 | 134 | Using Floats for the numerator or denominator is possible, however, because base-2 floats cannot represent all base-10 fractions properly, the results might be different from what you might expect. 135 | See [The Perils of Floating Point](http://www.lahey.com/float.htm) for more information about this. 136 | 137 | Floats are converted into rationals by using `Float.ratio` (since version 3.0). 138 | 139 | ## Decimals 140 | 141 | To use `Decimal` parameters, the [decimal](https://hex.pm/packages/decimal) library must 142 | be configured in `mix.exs`. 143 | 144 | ## Examples 145 | 146 | iex> Ratio.new(1, 2) 147 | Ratio.new(1, 2) 148 | iex> Ratio.new(100, 300) 149 | Ratio.new(1, 3) 150 | iex> Ratio.new(1.5, 4) 151 | Ratio.new(3, 8) 152 | iex> Ratio.new(Ratio.new(3, 2), 3) 153 | Ratio.new(1, 2) 154 | iex> Ratio.new(Ratio.new(3, 3), 2) 155 | Ratio.new(1, 2) 156 | iex> Ratio.new(Ratio.new(3, 2), Ratio.new(1, 3)) 157 | Ratio.new(9, 2) 158 | """ 159 | def new(numerator, denominator \\ 1) 160 | 161 | def new(_numerator, 0) do 162 | raise ArithmeticError 163 | end 164 | 165 | def new(numerator, denominator) when is_integer(numerator) and is_integer(denominator) do 166 | simplify(%Ratio{numerator: numerator, denominator: denominator}) 167 | end 168 | 169 | def new(numerator, denominator) when is_float(numerator) do 170 | div(Ratio.FloatConversion.float_to_rational(numerator), Ratio.new(denominator)) 171 | end 172 | 173 | def new(numerator, denominator) when is_float(denominator) do 174 | div(numerator, Ratio.FloatConversion.float_to_rational(denominator)) 175 | end 176 | 177 | def new(numerator = %Ratio{}, denominator = %Ratio{}) do 178 | div(numerator, denominator) 179 | end 180 | 181 | if Code.ensure_loaded?(Decimal) do 182 | def new(numerator = %Decimal{}, denominator = %Decimal{}) do 183 | Ratio.DecimalConversion.decimal_to_rational(numerator) 184 | |> div(Ratio.DecimalConversion.decimal_to_rational(denominator)) 185 | end 186 | 187 | def new(numerator = %Decimal{}, denominator) when is_float(denominator) do 188 | Ratio.DecimalConversion.decimal_to_rational(numerator) 189 | |> div(Ratio.FloatConversion.float_to_rational(denominator)) 190 | end 191 | 192 | def new(numerator, denominator = %Decimal{}) when is_float(numerator) do 193 | Ratio.FloatConversion.float_to_rational(numerator) 194 | |> div(Ratio.DecimalConversion.decimal_to_rational(denominator)) 195 | end 196 | 197 | def new(numerator = %Decimal{}, denominator) when is_integer(denominator) do 198 | Ratio.DecimalConversion.decimal_to_rational(numerator) 199 | |> div(denominator) 200 | end 201 | 202 | def new(numerator, denominator = %Decimal{}) when is_integer(numerator) do 203 | div(Ratio.DecimalConversion.decimal_to_rational(numerator), denominator) 204 | end 205 | end 206 | 207 | def new(numerator, denominator = %Ratio{}) when is_integer(numerator) do 208 | div(%Ratio{numerator: numerator, denominator: 1}, denominator) 209 | end 210 | 211 | def new(numerator = %Ratio{}, denominator) when is_integer(denominator) do 212 | div(numerator, %Ratio{numerator: denominator, denominator: 1}) 213 | end 214 | 215 | @doc """ 216 | Returns the absolute version of the given number (which might be an integer, float or Rational). 217 | 218 | ## Examples 219 | 220 | iex>Ratio.abs(Ratio.new(-5, 2)) 221 | Ratio.new(5, 2) 222 | """ 223 | def abs(number) when is_number(number), do: Kernel.abs(number) 224 | 225 | def abs(%Ratio{numerator: numerator, denominator: denominator}), 226 | do: Ratio.new(Kernel.abs(numerator), denominator) 227 | 228 | @doc """ 229 | Returns the sign of the given number (which might be an integer, float or Rational) 230 | 231 | This is: 232 | 233 | - 1 if the number is positive. 234 | - -1 if the number is negative. 235 | - 0 if the number is zero. 236 | 237 | """ 238 | def sign(%Ratio{numerator: numerator}) when Kernel.>(numerator, 0), do: 1 239 | def sign(%Ratio{numerator: numerator}) when Kernel.<(numerator, 0), do: Kernel.-(1) 240 | def sign(number) when is_number(number) and Kernel.>(number, 0), do: 1 241 | def sign(number) when is_number(number) and Kernel.<(number, 0), do: Kernel.-(1) 242 | def sign(number) when is_number(number), do: 0 243 | 244 | @doc """ 245 | Converts the passed *number* as a Rational number, and extracts its denominator. 246 | For integers returns the passed number itself. 247 | 248 | """ 249 | def numerator(number) when is_integer(number), do: number 250 | 251 | def numerator(number) when is_float(number), 252 | do: numerator(Ratio.FloatConversion.float_to_rational(number)) 253 | 254 | def numerator(%Ratio{numerator: numerator}), do: numerator 255 | 256 | @doc """ 257 | Treats the passed *number* as a Rational number, and extracts its denominator. 258 | For integers, returns `1`. 259 | """ 260 | def denominator(number) when is_number(number), do: 1 261 | def denominator(%Ratio{denominator: denominator}), do: denominator 262 | 263 | @doc """ 264 | Adds two rational numbers. 265 | 266 | iex> Ratio.add(Ratio.new(1, 4), Ratio.new(2, 4)) 267 | Ratio.new(3, 4) 268 | 269 | For ease of use, `rhs` is allowed to be an integer as well: 270 | 271 | iex> Ratio.add(Ratio.new(1, 4), 2) 272 | Ratio.new(9, 4) 273 | 274 | To perform addition where one of the operands might be another numeric type, 275 | use `Numbers.add/2` instead, as this will perform the required coercions 276 | between the number types: 277 | 278 | iex> Ratio.add(Ratio.new(1, 3), Decimal.new("3.14")) 279 | ** (FunctionClauseError) no function clause matching in Ratio.add/2 280 | 281 | iex> Numbers.add(Ratio.new(1, 3), Decimal.new("3.14")) 282 | Ratio.new(521, 150) 283 | """ 284 | def add(lhs, rhs) 285 | 286 | def add(%Ratio{numerator: a, denominator: lcm}, %Ratio{numerator: c, denominator: lcm}) do 287 | Ratio.new(Kernel.+(a, c), lcm) 288 | end 289 | 290 | def add(%Ratio{numerator: a, denominator: b}, %Ratio{numerator: c, denominator: d}) do 291 | Ratio.new(Kernel.+(a * d, c * b), b * d) 292 | end 293 | 294 | def add(lhs = %Ratio{}, rhs) when is_integer(rhs) do 295 | add(lhs, Ratio.new(rhs)) 296 | end 297 | 298 | @doc """ 299 | Subtracts the rational number *rhs* from the rational number *lhs*. 300 | 301 | iex> Ratio.sub(Ratio.new(1, 4), Ratio.new(2, 4)) 302 | Ratio.new(-1, 4) 303 | 304 | For ease of use, `rhs` is allowed to be an integer as well: 305 | 306 | iex> Ratio.sub(Ratio.new(1, 4), 2) 307 | Ratio.new(-7, 4) 308 | 309 | To perform addition where one of the operands might be another numeric type, 310 | use `Numbers.sub/2` instead, as this will perform the required coercions 311 | between the number types: 312 | 313 | iex> Ratio.sub(Ratio.new(1, 3), Decimal.new("3.14")) 314 | ** (FunctionClauseError) no function clause matching in Ratio.sub/2 315 | 316 | iex> Numbers.sub(Ratio.new(1, 3), Decimal.new("3.14")) 317 | Ratio.new(-421, 150) 318 | """ 319 | def sub(lhs, rhs) 320 | 321 | def sub(lhs = %Ratio{}, rhs = %Ratio{}), do: add(lhs, minus(rhs)) 322 | def sub(lhs = %Ratio{}, rhs) when is_integer(rhs), do: add(lhs, -rhs) 323 | 324 | @doc """ 325 | Negates the given rational number. 326 | 327 | ## Examples 328 | 329 | iex> Ratio.minus(Ratio.new(5, 3)) 330 | Ratio.new(-5, 3) 331 | """ 332 | def minus(%Ratio{numerator: numerator, denominator: denominator}) do 333 | %Ratio{numerator: Kernel.-(numerator), denominator: denominator} 334 | end 335 | 336 | @doc """ 337 | Multiplies two rational numbers. 338 | 339 | iex> Ratio.mult( Ratio.new(1, 3), Ratio.new(1, 2)) 340 | Ratio.new(1, 6) 341 | 342 | For ease of use, allows `rhs` to be an integer as well as a `Ratio` struct. 343 | 344 | iex> Ratio.mult( Ratio.new(1, 3), 2) 345 | Ratio.new(2, 3) 346 | 347 | To perform multiplication where one of the operands might be another numeric type, 348 | use `Numbers.mult/2` instead, as this will perform the required coercions 349 | between the number types: 350 | 351 | iex> Ratio.mult( Ratio.new(1, 3), Decimal.new("3.14")) 352 | ** (FunctionClauseError) no function clause matching in Ratio.mult/2 353 | 354 | iex> Numbers.mult( Ratio.new(1, 3), Decimal.new("3.14")) 355 | Ratio.new(157, 150) 356 | """ 357 | def mult(lhs, rhs) 358 | 359 | def mult(%Ratio{numerator: numerator1, denominator: denominator1}, %Ratio{ 360 | numerator: numerator2, 361 | denominator: denominator2 362 | }) do 363 | Ratio.new(Kernel.*(numerator1, numerator2), Kernel.*(denominator1, denominator2)) 364 | end 365 | 366 | def mult(lhs = %Ratio{}, rhs) when is_integer(rhs) do 367 | mult(lhs, Ratio.new(rhs)) 368 | end 369 | 370 | @doc """ 371 | Divides the rational number `lhs` by the rational number `rhs`. 372 | 373 | iex> Ratio.div(Ratio.new(2, 3), Ratio.new(8, 5)) 374 | Ratio.new(5, 12) 375 | 376 | For ease of use, allows `rhs` to be an integer as well as a `Ratio` struct. 377 | 378 | iex> Ratio.div(Ratio.new(2, 3), 10) 379 | Ratio.new(2, 30) 380 | 381 | To perform division where one of the operands might be another numeric type, 382 | use `Numbers.div/2` instead, as this will perform the required coercions 383 | between the number types: 384 | 385 | iex> Ratio.div(Ratio.new(2, 3), Decimal.new(10)) 386 | ** (FunctionClauseError) no function clause matching in Ratio.div/2 387 | 388 | iex> Numbers.div(Ratio.new(2, 3), Decimal.new(10)) 389 | Ratio.new(2, 30) 390 | """ 391 | def div(lhs, rhs) 392 | 393 | def div(%Ratio{numerator: numerator1, denominator: denominator1}, %Ratio{ 394 | numerator: numerator2, 395 | denominator: denominator2 396 | }) do 397 | Ratio.new(Kernel.*(numerator1, denominator2), Kernel.*(denominator1, numerator2)) 398 | end 399 | 400 | def div(lhs = %Ratio{}, rhs) when is_integer(rhs) do 401 | div(lhs, Ratio.new(rhs)) 402 | end 403 | 404 | defmodule ComparisonError do 405 | defexception message: "These things cannot be compared." 406 | end 407 | 408 | @doc """ 409 | Compares two rational numbers, returning `:lt`, `:eg` or `:gt` 410 | depending on whether *a* is less than, equal to or greater than *b*, respectively. 411 | 412 | This function is able to compare rational numbers against integers or floats as well. 413 | 414 | This function accepts other types as input as well, comparing them using Erlang's Term Ordering. 415 | This is mostly useful if you have a collection that contains other kinds of numbers (builtin integers or floats) as well. 416 | 417 | """ 418 | # TODO enhance this function to work with other number types? 419 | def compare(%Ratio{numerator: a, denominator: b}, %Ratio{numerator: c, denominator: d}) do 420 | compare(Kernel.*(a, d), Kernel.*(b, c)) 421 | end 422 | 423 | def compare(%Ratio{numerator: numerator, denominator: denominator}, b) do 424 | compare(numerator, Kernel.*(b, denominator)) 425 | end 426 | 427 | def compare(a, %Ratio{numerator: numerator, denominator: denominator}) do 428 | compare(Kernel.*(a, denominator), numerator) 429 | end 430 | 431 | # Fallback using the builting Erlang term ordering. 432 | def compare(a, b) do 433 | case {a, b} do 434 | {a, b} when a > b -> :gt 435 | {a, b} when a < b -> :lt 436 | _ -> :eq 437 | end 438 | end 439 | 440 | @doc """ 441 | True if *a* is equal to *b* 442 | """ 443 | def eq?(a, b), do: compare(a, b) |> Kernel.==(:eq) 444 | 445 | @doc """ 446 | True if *a* is larger than or equal to *b* 447 | """ 448 | def gt?(a, b), do: compare(a, b) |> Kernel.==(:gt) 449 | 450 | @doc """ 451 | True if *a* is smaller than *b* 452 | """ 453 | def lt?(a, b), do: compare(a, b) |> Kernel.==(:lt) 454 | 455 | @doc """ 456 | True if *a* is larger than or equal to *b* 457 | """ 458 | def gte?(a, b), do: compare(a, b) in [:eq, :gt] 459 | 460 | @doc """ 461 | True if *a* is smaller than or equal to *b* 462 | """ 463 | def lte?(a, b), do: compare(a, b) in [:lt, :eq] 464 | 465 | @doc """ 466 | True if *a* is equal to *b*? 467 | """ 468 | def equal?(a, b), do: compare(a, b) |> Kernel.==(:eq) 469 | 470 | @doc """ 471 | returns *x* to the *n* th power. 472 | 473 | *x* is allowed to be an integer, rational or float (in the last case, this is first converted to a rational). 474 | 475 | Will give the answer as a rational number when applicable. 476 | Note that the exponent *n* is only allowed to be an integer. 477 | 478 | (so it is not possible to compute roots using this function.) 479 | 480 | ## Examples 481 | 482 | iex> Ratio.pow(Ratio.new(2), 4) 483 | Ratio.new(16, 1) 484 | iex> Ratio.pow(Ratio.new(2), -4) 485 | Ratio.new(1, 16) 486 | iex> Ratio.pow(Ratio.new(3, 2), 10) 487 | Ratio.new(59049, 1024) 488 | iex> Ratio.pow(Ratio.new(10), 0) 489 | Ratio.new(1, 1) 490 | """ 491 | @spec pow(number() | Ratio.t(), pos_integer()) :: Ratio.t() 492 | def pow(x, n) 493 | 494 | # Convert Float to Rational. 495 | # def pow(x, n) when is_float(x), do: pow(Ratio.FloatConversion.float_to_rational(x), n) 496 | 497 | # Small powers 498 | def pow(%__MODULE__{}, 0), do: Ratio.new(1) 499 | def pow(x = %__MODULE__{}, 1), do: x 500 | def pow(x = %__MODULE__{}, 2), do: Ratio.mult(x, x) 501 | def pow(x = %__MODULE__{}, 3), do: Ratio.mult(Ratio.mult(x, x), x) 502 | def pow(x = %__MODULE__{}, n) when is_integer(n), do: do_pow(x, n) 503 | 504 | # Exponentiation By Squaring. 505 | defp do_pow(x, n, y \\ 1) 506 | defp do_pow(_x, 0, y), do: y 507 | defp do_pow(x, 1, y), do: Numbers.mult(x, y) 508 | defp do_pow(x, n, y) when Kernel.<(n, 0), do: do_pow(Ratio.new(1, x), Kernel.-(n), y) 509 | 510 | defp do_pow(x, n, y) when rem(n, 2) |> Kernel.==(0) do 511 | do_pow(Ratio.mult(x, x), Kernel.div(n, 2), y) 512 | end 513 | 514 | defp do_pow(x, n, y) do 515 | do_pow(Ratio.mult(x, x), Kernel.div(n - 1, 2), Numbers.mult(x, y)) 516 | end 517 | 518 | @doc """ 519 | Converts the given *number* to a Float. As floats do not have arbitrary precision, this operation is generally not reversible. 520 | """ 521 | @spec to_float(Ratio.t() | number) :: float 522 | def to_float(%Ratio{numerator: numerator, denominator: denominator}), 523 | do: Kernel./(numerator, denominator) 524 | 525 | def to_float(number), do: :erlang.float(number) 526 | 527 | @doc """ 528 | Returns a tuple, where the first element is the result of `to_float(number)` and 529 | the second is a conversion error. 530 | 531 | The conversion error is calculated by subtracting the original number from the 532 | conversion result. 533 | 534 | ## Examples 535 | 536 | iex> Ratio.to_float_error(Ratio.new(1, 2)) 537 | {0.5, Ratio.new(0, 1)} 538 | iex> Ratio.to_float_error(Ratio.new(2, 3)) 539 | {0.6666666666666666, Ratio.new(-1, 27021597764222976)} 540 | """ 541 | @spec to_float_error(t | number) :: {float, error} when error: t | number 542 | def to_float_error(number) do 543 | float = to_float(number) 544 | error = Ratio.sub(Ratio.new(float), number) 545 | {float, error} 546 | end 547 | 548 | @doc """ 549 | Returns a binstring representation of the Rational number. 550 | If the denominator is `1` it will still be printed wrapped with `Ratio.new`. 551 | 552 | ## Examples 553 | 554 | iex> Ratio.to_string Ratio.new(10, 7) 555 | "Ratio.new(10, 7)" 556 | iex> Ratio.to_string Ratio.new(10, 2) 557 | "Ratio.new(5, 1)" 558 | """ 559 | def to_string(rational) 560 | 561 | def to_string(%Ratio{numerator: numerator, denominator: denominator}) do 562 | "Ratio.new(#{numerator}, #{denominator})" 563 | end 564 | 565 | # defimpl String.Chars, for: Ratio do 566 | # def to_string(rational) do 567 | # Ratio.to_string(rational) 568 | # end 569 | # end 570 | 571 | defimpl Inspect, for: Ratio do 572 | def inspect(rational, _) do 573 | Ratio.to_string(rational) 574 | end 575 | end 576 | 577 | # Simplifies the Rational to its most basic form. 578 | # Which might result in an integer. 579 | # Ensures that a `-` is only kept in the numerator. 580 | defp simplify(rational) 581 | 582 | defp simplify(%Ratio{numerator: numerator, denominator: denominator}) do 583 | gcdiv = gcd(numerator, denominator) 584 | new_denominator = Kernel.div(denominator, gcdiv) 585 | {new_denominator, numerator} = normalize_denom_num(new_denominator, numerator) 586 | 587 | # if new_denominator == 1 do 588 | # Kernel.div(numerator, gcdiv) 589 | # else 590 | %Ratio{numerator: Kernel.div(numerator, gcdiv), denominator: new_denominator} 591 | # end 592 | end 593 | 594 | defp normalize_denom_num(denominator, numerator) do 595 | if denominator < 0 do 596 | {Kernel.-(denominator), Kernel.-(numerator)} 597 | else 598 | {denominator, numerator} 599 | end 600 | end 601 | 602 | # Calculates the Greatest Common denominator of two numbers. 603 | defp gcd(a, 0), do: abs(a) 604 | 605 | defp gcd(0, b), do: abs(b) 606 | defp gcd(a, b), do: gcd(b, Kernel.rem(a, b)) 607 | 608 | @doc """ 609 | Rounds a number (rational, integer or float) to the largest whole number less than or equal to num. 610 | For negative numbers, this means we are rounding towards negative infinity. 611 | 612 | 613 | iex> Ratio.floor(Ratio.new(1, 2)) 614 | 0 615 | iex> Ratio.floor(Ratio.new(5, 4)) 616 | 1 617 | iex> Ratio.floor(Ratio.new(-3, 2)) 618 | -2 619 | 620 | """ 621 | def floor(num) when is_integer(num), do: num 622 | def floor(num) when is_float(num), do: Float.floor(num) 623 | 624 | def floor(%Ratio{numerator: numerator, denominator: denominator}), 625 | do: Integer.floor_div(numerator, denominator) 626 | 627 | @doc """ 628 | Rounds a number (rational, integer or float) to the largest whole number larger than or equal to num. 629 | For negative numbers, this means we are rounding towards negative infinity. 630 | 631 | 632 | iex> Ratio.ceil(Ratio.new(1, 2)) 633 | 1 634 | iex> Ratio.ceil(Ratio.new(5, 4)) 635 | 2 636 | iex> Ratio.ceil(Ratio.new(-3, 2)) 637 | -1 638 | iex> Ratio.ceil(Ratio.new(400)) 639 | 400 640 | 641 | """ 642 | def ceil(num) when is_float(num), do: Float.ceil(num) 643 | def ceil(num) when is_integer(num), do: num 644 | 645 | def ceil(num = %Ratio{numerator: numerator, denominator: denominator}) do 646 | floor = Ratio.floor(num) 647 | 648 | if rem(numerator, denominator) == 0 do 649 | floor 650 | else 651 | floor + 1 652 | end 653 | end 654 | 655 | @doc """ 656 | Returns the integer part of number. 657 | 658 | ## Examples 659 | 660 | iex> Ratio.trunc(1.7) 661 | 1 662 | iex> Ratio.trunc(-1.7) 663 | -1 664 | iex> Ratio.trunc(3) 665 | 3 666 | iex> Ratio.trunc(Ratio.new(5, 2)) 667 | 2 668 | """ 669 | @spec trunc(t | number) :: integer 670 | def trunc(num) when is_integer(num), do: num 671 | def trunc(num) when is_float(num), do: Kernel.trunc(num) 672 | 673 | def trunc(%Ratio{numerator: numerator, denominator: denominator}) do 674 | Kernel.div(numerator, denominator) 675 | end 676 | end 677 | -------------------------------------------------------------------------------- /lib/ratio/coerce.ex: -------------------------------------------------------------------------------- 1 | require Coerce 2 | 3 | Coerce.defcoercion Ratio, Integer do 4 | def coerce(ratio, integer) do 5 | {ratio, Ratio.new(integer)} 6 | end 7 | end 8 | 9 | Coerce.defcoercion Ratio, Float do 10 | def coerce(ratio, float) do 11 | {ratio, Ratio.new(float)} 12 | end 13 | end 14 | 15 | if Code.ensure_loaded?(Decimal) do 16 | Coerce.defcoercion Ratio, Decimal do 17 | def coerce(ratio, decimal) do 18 | {ratio, Ratio.DecimalConversion.decimal_to_rational(decimal)} 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/ratio/decimal_conversion.ex: -------------------------------------------------------------------------------- 1 | if Code.ensure_loaded?(Decimal) do 2 | defmodule Ratio.DecimalConversion do 3 | def decimal_to_rational(%Decimal{coef: coef, exp: 0, sign: sign}) do 4 | numerator = coef * sign 5 | Ratio.new(numerator) 6 | end 7 | 8 | def decimal_to_rational(%Decimal{coef: coef, exp: exp, sign: sign}) do 9 | numerator = coef * sign 10 | denominator = Ratio.pow(Ratio.new(10), exp * -1) 11 | Ratio.new(numerator, denominator) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/ratio/float_conversion.ex: -------------------------------------------------------------------------------- 1 | defmodule Ratio.FloatConversion do 2 | @doc """ 3 | Converts a float to a rational number. 4 | Because base-2 floats cannot represent all base-10 fractions properly, the results might be different from what you might expect. 5 | See [The Perils of Floating Point](http://www.lahey.com/float.htm) for more information about this. 6 | 7 | ## Examples 8 | 9 | iex> Ratio.FloatConversion.float_to_rational(10.0) 10 | Ratio.new(10, 1) 11 | iex> Ratio.FloatConversion.float_to_rational(13.5) 12 | Ratio.new(27, 2) 13 | iex> Ratio.FloatConversion.float_to_rational(1.1) 14 | Ratio.new(2476979795053773, 2251799813685248) 15 | """ 16 | 17 | def float_to_rational(float) do 18 | {numerator, denominator} = Float.ratio(float) 19 | Ratio.new(numerator, denominator) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/ratio/numbers.ex: -------------------------------------------------------------------------------- 1 | # Unary protocols: 2 | defimpl Numbers.Protocols.Minus, for: Ratio do 3 | def minus(val) do 4 | Ratio.minus(val) 5 | end 6 | end 7 | 8 | defimpl Numbers.Protocols.Absolute, for: Ratio do 9 | def abs(val) do 10 | Ratio.abs(val) 11 | end 12 | end 13 | 14 | defimpl Numbers.Protocols.ToFloat, for: Ratio do 15 | def to_float(val) do 16 | Ratio.to_float(val) 17 | end 18 | end 19 | 20 | # Binary protocols: 21 | defimpl Numbers.Protocols.Addition, for: Ratio do 22 | def add(lhs, rhs) do 23 | Ratio.add(lhs, rhs) 24 | end 25 | 26 | def add_id(_) do 27 | Ratio.new(0) 28 | end 29 | end 30 | 31 | defimpl Numbers.Protocols.Subtraction, for: Ratio do 32 | def sub(lhs, rhs) do 33 | Ratio.sub(lhs, rhs) 34 | end 35 | end 36 | 37 | defimpl Numbers.Protocols.Multiplication, for: Ratio do 38 | def mult(lhs, rhs) do 39 | Ratio.mult(lhs, rhs) 40 | end 41 | 42 | def mult_id(_) do 43 | Ratio.new(1) 44 | end 45 | end 46 | 47 | defimpl Numbers.Protocols.Division, for: Ratio do 48 | def div(lhs, rhs) do 49 | Ratio.div(lhs, rhs) 50 | end 51 | end 52 | 53 | defimpl Numbers.Protocols.Exponentiation, for: Ratio do 54 | def pow(lhs, integer_power) do 55 | Ratio.pow(lhs, integer_power) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Rational.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ratio, 7 | version: "4.0.1", 8 | elixir: "~> 1.6", 9 | build_embedded: Mix.env() == :prod, 10 | start_permanent: Mix.env() == :prod, 11 | deps: deps(), 12 | docs: docs(), 13 | package: package(), 14 | description: description(), 15 | source_url: "https://github.com/qqwy/elixir-rational" 16 | ] 17 | end 18 | 19 | # Configuration for the OTP application 20 | # 21 | # Type "mix help compile.app" for more information 22 | def application do 23 | extra_applications = 24 | [:numbers, :logger] ++ case Mix.env() do 25 | :test -> [:stream_data] 26 | _ -> [] 27 | end 28 | 29 | [ 30 | extra_applications: extra_applications 31 | ] 32 | end 33 | 34 | # Dependencies can be Hex packages: 35 | # 36 | # {:mydep, "~> 0.3.0"} 37 | # 38 | # Or git/path repositories: 39 | # 40 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 41 | # 42 | # Type "mix help deps" for more examples and options 43 | defp deps do 44 | [ 45 | # Markdown, dependency of ex_doc 46 | {:earmark, ">= 1.0.0", only: [:dev]}, 47 | # Documentation for Hex.pm 48 | {:ex_doc, "~> 0.20", only: [:dev]}, 49 | # Generic arithmetic dispatching. 50 | {:numbers, "~> 5.2.0"}, 51 | # If Decimal number support is required 52 | {:decimal, "~> 1.6 or ~> 2.0", optional: true}, 53 | {:stream_data, "~> 0.1", only: [:dev, :test]} 54 | ] 55 | end 56 | 57 | defp package do 58 | [ 59 | files: ["lib", "mix.exs", "README*", "LICENSE*"], 60 | maintainers: ["Qqwy/WM"], 61 | licenses: ["MIT"], 62 | links: %{github: "https://github.com/qqwy/elixir-rational"} 63 | ] 64 | end 65 | 66 | defp description do 67 | """ 68 | This library allows you to use Rational numbers in Elixir, to enable exact calculations with all numbers big and small. 69 | """ 70 | end 71 | 72 | defp docs do 73 | [ 74 | main: "readme", 75 | extras: [ 76 | "README.md": [title: "Guide/Readme"] 77 | ] 78 | ] 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"}, 3 | "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, 4 | "earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, 6 | "ex_doc": {:hex, :ex_doc, "0.31.2", "8b06d0a5ac69e1a54df35519c951f1f44a7b7ca9a5bb7a260cd8a174d6322ece", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "317346c14febaba9ca40fd97b5b5919f7751fb85d399cc8e7e8872049f37e0af"}, 7 | "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, 8 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, 9 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, 10 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 11 | "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, 12 | "stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"}, 13 | } 14 | -------------------------------------------------------------------------------- /test/ratio/decimal_conversion_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ratio.DecimalConversionTest do 2 | use ExUnit.Case, async: true 3 | 4 | @test_cases [ 5 | {Decimal.new("-1230"), Ratio.new(-1230)}, 6 | {Decimal.new("-123"), Ratio.new(-123)}, 7 | {Decimal.new("-12.3"), Ratio.new(-123, 10)}, 8 | {Decimal.new("-1.23"), Ratio.new(-123, 100)}, 9 | {Decimal.new("-0.123"), Ratio.new(-123, 1000)}, 10 | {Decimal.new("-0.0123"), Ratio.new(-123, 10000)}, 11 | {Decimal.new("1230"), Ratio.new(1230)}, 12 | {Decimal.new("123"), Ratio.new(123)}, 13 | {Decimal.new("12.3"), Ratio.new(123, 10)}, 14 | {Decimal.new("1.23"), Ratio.new(123, 100)}, 15 | {Decimal.new("0.123"), Ratio.new(123, 1000)}, 16 | {Decimal.new("0.0123"), Ratio.new(123, 10000)} 17 | ] 18 | 19 | for {input, output} <- @test_cases do 20 | test "Proper decimal-> ratio conversion for #{input}" do 21 | assert Ratio.DecimalConversion.decimal_to_rational(unquote(Macro.escape(input))) == 22 | unquote(Macro.escape(output)) 23 | end 24 | end 25 | 26 | test "Create a ratio with only a Decimal numerator and no denominator (regression test for #111)" do 27 | assert Ratio.new(Decimal.new(1)) == Ratio.new(1, 1) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/ratio/float_conversion_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ratio.FloatConversionTest do 2 | use ExUnit.Case, async: true 3 | 4 | # use Ratio coerces negative floats to Ratios, so the below test needs to be run outside the Ratio.FloatConversion 5 | # module. 6 | test "float conversion for negative numbers" do 7 | assert %Ratio{numerator: -2_476_979_795_053_773, denominator: 2_251_799_813_685_248} == 8 | Ratio.FloatConversion.float_to_rational(-1.1) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/ratio/numbers_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ratio.NumbersTest do 2 | use ExUnit.Case, async: true 3 | import Ratio, only: [is_rational: 1] 4 | use ExUnitProperties 5 | import TestHelper 6 | 7 | alias Numbers, as: N 8 | use Numbers, overload_operators: true 9 | 10 | @unary_operations [:abs, :minus, :to_float] 11 | for operation <- @unary_operations do 12 | test "Numbers.#{operation}/1 has the same result as running Ratio.#{operation}/1 (except casting)" do 13 | assert Ratio.unquote(operation)(Ratio.new(1, 3)) == N.unquote(operation)(Ratio.new(1, 3)) 14 | end 15 | end 16 | 17 | @binary_operations [:add, :sub, :mult, :div] 18 | for operation <- @binary_operations do 19 | test "Numbers.#{operation}/2 has the same result as running Ratio.#{operation}/2 (except casting)" do 20 | assert Ratio.unquote(operation)(Ratio.new(1, 2), Ratio.new(3)) == 21 | N.unquote(operation)(Ratio.new(1, 2), Ratio.new(3)) 22 | end 23 | end 24 | 25 | test "Numbers.pow/2 has the same result as running Ratio.pos/2 (except casting)" do 26 | assert Ratio.pow(Ratio.new(1, 2), 3) == N.pow(Ratio.new(1, 2), 3) 27 | end 28 | 29 | property "Addition is closed" do 30 | check all a <- rational_generator(), 31 | b <- rational_generator() do 32 | assert is_rational(a + b) 33 | end 34 | end 35 | 36 | property "Addition is commutative" do 37 | check all a <- rational_generator(), 38 | b <- rational_generator() do 39 | assert a + b == b + a 40 | end 41 | end 42 | 43 | property "Addition is associative" do 44 | check all a <- rational_generator(), 45 | b <- rational_generator(), 46 | c <- rational_generator() do 47 | assert a + b + c == a + (b + c) 48 | end 49 | end 50 | 51 | property "Additive identity" do 52 | check all a <- rational_generator() do 53 | assert a + 0 == a 54 | assert 0 + a == a 55 | end 56 | end 57 | 58 | property "Additive inverse" do 59 | check all a <- rational_generator() do 60 | inverse = Ratio.new(-a.numerator, a.denominator) 61 | assert a + inverse == Ratio.new(0) 62 | assert inverse + a == Ratio.new(0) 63 | end 64 | end 65 | 66 | property "Subtraction is closed" do 67 | check all a <- rational_generator(), 68 | b <- rational_generator() do 69 | assert is_rational(a - b) 70 | end 71 | end 72 | 73 | property "Subtractive inverse" do 74 | check all a <- rational_generator() do 75 | inverse = Ratio.new(-a.numerator, a.denominator) 76 | assert 0 - inverse == a 77 | assert 0 - a == inverse 78 | end 79 | end 80 | 81 | property "Multiplication is closed" do 82 | check all a <- rational_generator(), 83 | b <- rational_generator() do 84 | assert is_rational(a * b) 85 | end 86 | end 87 | 88 | property "Multiplication is commutative" do 89 | check all a <- rational_generator(), 90 | b <- rational_generator() do 91 | assert a * b == b * a 92 | end 93 | end 94 | 95 | property "Multiplication is associative" do 96 | check all a <- rational_generator(), 97 | b <- rational_generator(), 98 | c <- rational_generator() do 99 | assert a * b * c == a * (b * c) 100 | end 101 | end 102 | 103 | property "Multiplicative identity" do 104 | check all a <- rational_generator() do 105 | assert a * 1 == a 106 | assert 1 * a == a 107 | end 108 | end 109 | 110 | property "Multiplication by zero is always zero" do 111 | check all a <- rational_generator() do 112 | assert a * 0 == Ratio.new(0) 113 | assert 0 * a == Ratio.new(0) 114 | end 115 | end 116 | 117 | property "Division is closed" do 118 | check all a <- rational_generator(), 119 | b <- rational_generator(), 120 | b != Ratio.new(0) do 121 | assert is_rational(a / b) 122 | end 123 | end 124 | 125 | property "Multiplication distributes over Addition" do 126 | check all a <- rational_generator(), 127 | b <- rational_generator(), 128 | c <- rational_generator() do 129 | assert a * (b + c) == a * b + a * c 130 | end 131 | end 132 | 133 | property "Multiplication distributes over Subtraction" do 134 | check all a <- rational_generator(), 135 | b <- rational_generator(), 136 | c <- rational_generator() do 137 | assert a * (b - c) == a * b - a * c 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /test/ratio_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RatioTest do 2 | use ExUnit.Case, async: true 3 | use ExUnitProperties 4 | import TestHelper 5 | 6 | import Ratio, only: [is_rational: 1] 7 | 8 | # Example used by a module-doc doctest in the Ratio module 9 | defmodule IDoAlotOfMathHere do 10 | defdelegate numerator <~> denominator, to: Ratio, as: :new 11 | use Numbers, overload_operators: true 12 | 13 | def calculate(input) do 14 | num = input <~> 2 15 | result = num * 2 + (3 <~> 4) * 5.0 16 | result / 2 17 | end 18 | end 19 | 20 | doctest Ratio 21 | doctest Ratio.FloatConversion 22 | 23 | # This way we can use the shorthand syntax in this module 24 | # And 'test' it at the same time :-) 25 | defdelegate numerator <~> denominator, to: Ratio, as: :new 26 | 27 | test "definition of <~> operator" do 28 | assert 1 <~> 3 == %Ratio{numerator: 1, denominator: 3} 29 | end 30 | 31 | test "reject _ <~> 0" do 32 | assert_raise ArithmeticError, fn -> 1 <~> 0 end 33 | assert_raise ArithmeticError, fn -> 1234 <~> 0 end 34 | end 35 | 36 | test "inspect protocol" do 37 | assert Inspect.inspect(Ratio.new(1, 2), []) == "Ratio.new(1, 2)" 38 | end 39 | 40 | test "compare/2" do 41 | assert Ratio.compare(1, 2) == :lt 42 | assert Ratio.compare(2, 1) == :gt 43 | assert Ratio.compare(1 <~> 2, 2 <~> 3) == :lt 44 | assert Ratio.compare(1 <~> 2, 2 <~> 4) == :eq 45 | end 46 | 47 | test "lt?/2, lte?/2, gt?/1, gte?/2, equal?/2" do 48 | assert Ratio.lt?(1 <~> 2, 1) 49 | refute Ratio.lt?(2, 1 <~> 2) 50 | refute Ratio.lt?(1 <~> 2, 1 <~> 2) 51 | 52 | refute Ratio.gt?(1 <~> 2, 1) 53 | assert Ratio.gt?(1 <~> 2, 1 <~> 4) 54 | 55 | assert Ratio.gte?(1 <~> 2, 1 <~> 4) 56 | assert Ratio.gte?(1 <~> 2, 1 <~> 2) 57 | refute Ratio.gte?(1 <~> 4, 1 <~> 2) 58 | 59 | assert Ratio.lte?(1 <~> 4, 1 <~> 2) 60 | assert Ratio.lte?(1 <~> 2, 1 <~> 2) 61 | refute Ratio.lte?(1 <~> 2, 1 <~> 4) 62 | 63 | assert Ratio.equal?(1 <~> 3, 1 <~> 3) 64 | refute Ratio.equal?(1 <~> 3, 1 <~> 4) 65 | end 66 | 67 | test "small number precision" do 68 | assert Ratio.equal?( 69 | Ratio.new(1.602177e-19), 70 | 1_663_795_720_783_351 <~> 10_384_593_717_069_655_257_060_992_658_440_192 71 | ) 72 | 73 | assert Ratio.equal?( 74 | Ratio.new(1.49241808560e-10), 75 | 5_773_512_823_493_363 <~> 38_685_626_227_668_133_590_597_632 76 | ) 77 | end 78 | 79 | property "Addition is closed" do 80 | check all a <- rational_generator(), 81 | b <- rational_generator() do 82 | assert is_rational(Ratio.add(a, b)) 83 | end 84 | end 85 | 86 | property "Addition is commutative" do 87 | check all a <- rational_generator(), 88 | b <- rational_generator() do 89 | assert Ratio.add(a, b) == Ratio.add(b, a) 90 | end 91 | end 92 | 93 | property "Addition is associative" do 94 | check all a <- rational_generator(), 95 | b <- rational_generator(), 96 | c <- rational_generator() do 97 | assert Ratio.add(Ratio.add(a, b), c) == Ratio.add(a, Ratio.add(b, c)) 98 | end 99 | end 100 | 101 | property "Additive identity" do 102 | check all a <- rational_generator() do 103 | assert Ratio.add(a, Ratio.new(0)) == a 104 | assert Ratio.add(Ratio.new(0), a) == a 105 | end 106 | end 107 | 108 | property "Additive inverse" do 109 | check all a <- rational_generator() do 110 | inverse = Ratio.new(-a.numerator, a.denominator) 111 | assert Ratio.add(a, inverse) == Ratio.new(0) 112 | assert Ratio.add(inverse, a) == Ratio.new(0) 113 | end 114 | end 115 | 116 | property "Subtraction is closed" do 117 | check all a <- rational_generator(), 118 | b <- rational_generator() do 119 | assert is_rational(Ratio.sub(a, b)) 120 | end 121 | end 122 | 123 | property "Subtractive inverse" do 124 | check all a <- rational_generator() do 125 | inverse = Ratio.new(-a.numerator, a.denominator) 126 | assert Ratio.sub(Ratio.new(0), inverse) == a 127 | assert Ratio.sub(Ratio.new(0), a) == inverse 128 | end 129 | end 130 | 131 | property "Multiplication is closed" do 132 | check all a <- rational_generator(), 133 | b <- rational_generator() do 134 | assert is_rational(Ratio.mult(a, b)) 135 | end 136 | end 137 | 138 | property "Multiplication is commutative" do 139 | check all a <- rational_generator(), 140 | b <- rational_generator() do 141 | assert Ratio.mult(a, b) == Ratio.mult(b, a) 142 | end 143 | end 144 | 145 | property "Multiplication is associative" do 146 | check all a <- rational_generator(), 147 | b <- rational_generator(), 148 | c <- rational_generator() do 149 | assert Ratio.mult(Ratio.mult(a, b), c) == Ratio.mult(a, Ratio.mult(b, c)) 150 | end 151 | end 152 | 153 | property "Multiplicative identity" do 154 | check all a <- rational_generator() do 155 | assert Ratio.mult(a, Ratio.new(1)) == a 156 | assert Ratio.mult(Ratio.new(1), a) == a 157 | end 158 | end 159 | 160 | property "Multiplication by zero is always zero" do 161 | check all a <- rational_generator() do 162 | assert Ratio.mult(a, Ratio.new(0)) == Ratio.new(0) 163 | assert Ratio.mult(Ratio.new(0), a) == Ratio.new(0) 164 | end 165 | end 166 | 167 | property "Division is closed" do 168 | check all a <- rational_generator(), 169 | b <- rational_generator(), 170 | b != Ratio.new(0) do 171 | assert is_rational(Ratio.div(a, b)) 172 | end 173 | end 174 | 175 | property "Multiplication distributes over Addition" do 176 | check all a <- rational_generator(), 177 | b <- rational_generator(), 178 | c <- rational_generator() do 179 | left = Ratio.mult(a, Ratio.add(b, c)) 180 | right = Ratio.add(Ratio.mult(a, b), Ratio.mult(a, c)) 181 | assert left == right 182 | end 183 | end 184 | 185 | property "Multiplication distributes over Subtraction" do 186 | check all a <- rational_generator(), 187 | b <- rational_generator(), 188 | c <- rational_generator() do 189 | left = Ratio.mult(a, Ratio.sub(b, c)) 190 | right = Ratio.sub(Ratio.mult(a, b), Ratio.mult(a, c)) 191 | assert left == right 192 | end 193 | end 194 | 195 | test "When ceiling a number with denominator 1 it returns nominator" do 196 | # https://github.com/Qqwy/elixir-rational/issues/89#issuecomment-1139664334 197 | assert Ratio.new(400) |> Ratio.ceil() == 400 198 | end 199 | end 200 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | require ExUnitProperties 4 | 5 | defmodule TestHelper do 6 | def rational_generator do 7 | ExUnitProperties.gen all numerator <- StreamData.integer(), 8 | denominator <- StreamData.integer(), 9 | denominator != 0 do 10 | Ratio.new(numerator, denominator) 11 | end 12 | end 13 | end 14 | --------------------------------------------------------------------------------