├── .github ├── FUNDING.yml ├── renovate.json └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── lib └── custom_base.ex ├── mix.exs ├── mix.lock └── test ├── custom_base_test.exs └── test_helper.exs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 2 | - igas 3 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["igas/.github"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | - pull_request 4 | - push 5 | jobs: 6 | mix_test: 7 | name: mix test (Elixir ${{ matrix.elixir }} OTP ${{ matrix.otp }}) 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | include: 13 | - elixir: '1.12.x' 14 | otp: '24.x' 15 | - elixir: '1.13.x' 16 | otp: '24.x' 17 | - elixir: '1.14.x' 18 | otp: '25.x' 19 | - elixir: '1.15.x' 20 | otp: '26.x' 21 | - elixir: '1.16.x' 22 | otp: '26.x' 23 | steps: 24 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 25 | - uses: erlef/setup-beam@0a541161e47ec43ccbd9510053c5f336ca76c2a2 # v1.17.6 26 | with: 27 | otp-version: ${{ matrix.otp }} 28 | elixir-version: ${{ matrix.elixir }} 29 | - name: Install Dependencies 30 | run: | 31 | mix local.hex --force 32 | mix local.rebar --force 33 | mix deps.get --only test 34 | - name: Run tests 35 | run: mix test 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | custom_base-*.tar 24 | 25 | # Temporary files for e.g. tests 26 | /tmp 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.2.1][] - 2017-04-17 11 | ### Changed 12 | - Fix deprecations [@myobie][] 13 | 14 | ## [0.2.0][] - 2016-02-01 15 | ### Changed 16 | - Fix overflow caused by float :math.pow [@drewblas][] 17 | 18 | ## [0.1.0][] - 2015-03-12 19 | ### Added 20 | - Initial release [@igas][] 21 | 22 | [@drewblas]: https://github.com/drewblas 23 | [@igas]: https://github.com/igas 24 | [@myobie]: https://github.com/myobie 25 | [0.2.1]: https://github.com/igas/custom_base/compare/v0.2.0...v0.2.1 26 | [0.2.0]: https://github.com/igas/custom_base/compare/v0.1.0...v0.2.0 27 | [0.1.0]: https://github.com/igas/custom_base/compare/2b2fad2...v0.1.0 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Igor Kapkov 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 | # CustomBase 2 | 3 | [![Test](https://github.com/igas/custom_base/workflows/Test/badge.svg)](https://github.com/igas/custom_base/actions?query=workflow%3ATest) 4 | [![Module Version](https://img.shields.io/hexpm/v/custom_base.svg)](https://hex.pm/packages/custom_base) 5 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/custom_base/) 6 | [![Total Download](https://img.shields.io/hexpm/dt/custom_base.svg)](https://hex.pm/packages/custom_base) 7 | [![License](https://img.shields.io/hexpm/l/custom_base.svg)](https://github.com/igas/custom_base/blob/main/LICENSE) 8 | [![Last Updated](https://img.shields.io/github/last-commit/igas/custom_base.svg)](https://github.com/igas/custom_base/commits/main) 9 | 10 | CustomBase allow you to make custom base conversion in Elixir. 11 | 12 | ## Installation 13 | 14 | Add `{:custom_base, "~> 0.2"}` to `deps` function in your `mix.exs` file. 15 | 16 | After you are done, run `mix deps.get` in your shell to fetch and compile CustomBase. 17 | 18 | ## Examples 19 | 20 | Lets make `Base12` module with conversion described below. 21 | 22 | | Value | Encoding | 23 | |------:|---------:| 24 | | 0| 0| 25 | | 1| 1| 26 | | 2| 2| 27 | | 3| 3| 28 | | 4| 4| 29 | | 5| 5| 30 | | 6| 6| 31 | | 7| 7| 32 | | 8| 8| 33 | | 9| 9| 34 | | 10| A| 35 | | 11| B| 36 | 37 | Add macro to your module: 38 | 39 | ```elixir 40 | defmodule Base12 do 41 | use CustomBase, '0123456789AB' 42 | end 43 | ``` 44 | 45 | Now your module have 2 functions `encode/1` and `decode/1`: 46 | 47 | ``` 48 | iex> Base12.encode(9) 49 | "9" 50 | iex> Base12.encode(10) 51 | "A" 52 | iex> Base12.encode(11) 53 | "B" 54 | iex> Base12.encode(12) 55 | "10" 56 | iex> Base12.decode("16") 57 | 18 58 | iex> Base12.decode("AB") 59 | 131 60 | ``` 61 | 62 | ## Specs & Docs 63 | 64 | All specs included by this library, if you want to provide your documentation, 65 | add it after `use` call, like this: 66 | 67 | ``` 68 | defmodule Base12 do 69 | use CustomBase, '0123456789AB' 70 | 71 | @moduledoc """ 72 | Your module docs. 73 | """ 74 | 75 | @doc """ 76 | Documentation for encode/1. 77 | """ 78 | def encode(integer) 79 | 80 | @doc """ 81 | Documentation for decode/1. 82 | """ 83 | def decode(binary) 84 | 85 | @doc """ 86 | Documentation for decode!/1. 87 | """ 88 | def decode!(binary) 89 | end 90 | ``` 91 | 92 | ## License 93 | 94 | Copyright (c) 2015 Igor Kapkov 95 | 96 | This library is MIT licensed. See the [LICENSE](https://github.com/igas/custom_base/blob/main/LICENSE) 97 | for details. 98 | -------------------------------------------------------------------------------- /lib/custom_base.ex: -------------------------------------------------------------------------------- 1 | defmodule CustomBase do 2 | @moduledoc false 3 | 4 | defmodule Pow do 5 | @moduledoc false 6 | # This is an integer-only implementation of pow. 7 | # It avoids issues where the erlang :math.pow overflows on a float 8 | # but yet doesn't raise any errors. 9 | 10 | require Integer 11 | 12 | def pow(_, 0), do: 1 13 | def pow(x, n) when Integer.is_odd(n), do: x * pow(x, n - 1) 14 | def pow(x, n) do 15 | result = pow(x, div(n, 2)) 16 | result * result 17 | end 18 | end 19 | 20 | defmacro __using__(alphabet) do 21 | quote bind_quoted: [alphabet: alphabet] do 22 | for { digit, idx } <- Enum.with_index(alphabet) do 23 | def encode(unquote(idx)), do: unquote(<< digit >>) 24 | end 25 | 26 | @spec encode(integer) :: binary 27 | def encode(number) do 28 | encode(div(number, unquote(length(alphabet)))) <> encode(rem(number, unquote(length(alphabet)))) 29 | end 30 | 31 | @spec decode(binary) :: {:ok, integer} | :error 32 | def decode(binary) do 33 | { :ok, do_decode(binary) } 34 | rescue 35 | ArgumentError -> :error 36 | end 37 | 38 | @spec decode!(binary) :: integer 39 | def decode!(binary), do: do_decode(binary) 40 | 41 | defp do_decode(binary) do 42 | binary 43 | |> String.split("", trim: true) 44 | |> Enum.reverse 45 | |> decode(0) 46 | end 47 | 48 | for { digit, idx } <- Enum.with_index(alphabet) do 49 | defp decode_char(unquote(<< digit >>)), do: unquote(idx) 50 | end 51 | 52 | defp decode_char(char) do 53 | raise(ArgumentError, "non-alphabet digit found: #{char}") 54 | end 55 | 56 | defp decode([last], step) do 57 | decode_char(last) * Pow.pow(unquote(length(alphabet)), step) 58 | end 59 | 60 | defp decode([head|tail], step) do 61 | decode_char(head) * Pow.pow(unquote(length(alphabet)), step) + decode(tail, step + 1) 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule CustomBase.Mixfile do 2 | use Mix.Project 3 | 4 | @version "0.2.1" 5 | @github "https://github.com/igas/custom_base" 6 | 7 | def project do 8 | [ 9 | app: :custom_base, 10 | version: @version, 11 | elixir: "~> 1.0", 12 | name: "CustomBase", 13 | description: description(), 14 | package: package(), 15 | source_url: @github, 16 | docs: docs(), 17 | deps: deps() 18 | ] 19 | end 20 | 21 | def application, do: [] 22 | 23 | defp docs do 24 | [ 25 | extras: ["CHANGELOG.md", "README.md"], 26 | main: "readme", 27 | name: "custom_base", 28 | canonical: "https://hexdocs.pm/custom_base", 29 | source_ref: "v#{@version}", 30 | source_url: @github, 31 | api_reference: false 32 | ] 33 | end 34 | 35 | defp deps do 36 | [ 37 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} 38 | ] 39 | end 40 | 41 | defp description do 42 | "Allow you to make custom base conversion in Elixir." 43 | end 44 | 45 | defp package do 46 | [ 47 | maintainers: ["Igor Kapkov"], 48 | files: ["lib", "mix.exs", "CHANGELOG.md", "README.md", "LICENSE"], 49 | licenses: ["MIT"], 50 | links: %{ 51 | "Changelog" => "https://hexdocs.pm/custom_base/changelog.html", 52 | "GitHub" => @github 53 | } 54 | ] 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"earmark": {:hex, :earmark, "1.4.13", "2c6ce9768fc9fdbf4046f457e207df6360ee6c91ee1ecb8e9a139f96a4289d91", [:mix], [{:earmark_parser, ">= 1.4.12", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "a0cf3ed88ef2b1964df408889b5ecb886d1a048edde53497fc935ccd15af3403"}, 2 | "earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"}, 3 | "ex_doc": {:hex, :ex_doc, "0.23.0", "a069bc9b0bf8efe323ecde8c0d62afc13d308b1fa3d228b65bca5cf8703a529d", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f5e2c4702468b2fd11b10d39416ddadd2fcdd173ba2a0285ebd92c39827a5a16"}, 4 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 5 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"}, 6 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}} 7 | -------------------------------------------------------------------------------- /test/custom_base_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BaseABCDEFGHIJ do 2 | use CustomBase, 'abcdefghij' 3 | end 4 | 5 | defmodule HexBase do 6 | use CustomBase, '0123456789abcdef' 7 | end 8 | 9 | defmodule CustomBaseTest do 10 | use ExUnit.Case 11 | import BaseABCDEFGHIJ 12 | 13 | test "encode/1" do 14 | assert encode(0) == "a" 15 | assert encode(9) == "j" 16 | assert encode(11) == "bb" 17 | end 18 | 19 | test "decode/1" do 20 | assert decode("c") == {:ok, 2} 21 | assert decode("i") == {:ok, 8} 22 | assert decode("gg") == {:ok, 66} 23 | end 24 | 25 | test "decode!/1" do 26 | assert decode!("c") == 2 27 | assert decode!("i") == 8 28 | assert decode!("gg") == 66 29 | end 30 | 31 | # This test will fail if :math.pow overflows 32 | test "pow overflow" do 33 | int = 489732498728927498273432 34 | str = "67b473b32ffa144f0298" # Checked with WolframAlpha 35 | 36 | assert str == HexBase.encode(int) 37 | assert int == HexBase.decode!(str) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | --------------------------------------------------------------------------------