How to make a custom positional notation system in Ruby 🧮
Tags: ruby, math, programming, learning
Published: 2023-12-18 12:40:49

In Ruby you can easily convert positional numbers that have their bases between 2 and 36. But you don’t have direct access to their alphabets (digits of which these systems are composed of) and you can’t customise them. We can only deduce that the alphabet of a 36 base system is a combination of all decimal digits and English alphabet, that is:

```ruby
irb(main)> ['0'..'9', 'a'..'z']
  .map(&:to_a)
  .flatten
  .size
=> 36
irb(main)> 189.to_s(17)
=> "b2"
irb(main)> 35.to_s(36)
=> "z"
```

We can also play the other way around:

```ruby
irb(main)> "1001110".to_i(2)
=> 78
irb(main)> "z".to_i(36)
=> 35
```

To build a binary system with digits X and Y you need to write your own code. Also, you can’t have bases bigger than 36 out of the box. And if you come from [Babylonian numeral system](https://en.wikipedia.org/wiki/Babylonian_cuneiform_numerals) this is a real issue!

![Babylonian numeral system](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4s9lncmmuyymalfhtaux.png)

Although, if you look at it, it’s quite decimal.

Now imagine you want to write a service to perform positional notation calculations on a string composed of specific digits. For example, if you pass an array like this:

* `[7, 'X', 'h']`

You want to get an object that would return:

* `0` when you call `'7'`
* `1` when you call `'X'`
* `2` when you call `'h'`
* multi-digit arguments should work as well

The base for the given array would be 3 because there are 3 elements. Let’s put some requirements:

```ruby
require 'rspec'

PositionalNotation = Class.new

RSpec.describe PositionalNotation do
  subject(:b3) do
    described_class.new(custom_digits)
  end

  let(:custom_digits) { %w[7 X h] }

  it { expect(b3.call('7')).to eq(0) }
  it { expect(b3.call('X')).to eq(1) }
  it { expect(b3.call('h')).to eq(2) }
  it { expect(b3.call('X7')).to eq(3) }
  it { expect(b3.call('XX')).to eq(4) }
  it { expect(b3.call('Xh')).to eq(5) }
  it { expect(b3.call('h7')).to eq(6) }
  it { expect(b3.call('hX')).to eq(7) }
  it { expect(b3.call('hh')).to eq(8) }
end
```

Okay, so what’s the actual code of this class?

```ruby
class PositionalNotation
  attr_reader :digits, :base

  def initialize(digits)
    @digits = digits
    @base = digits.size
  end

  def call(number)
    number
      .each_char
      .reverse_each
      .with_index
      .reduce(0) do |sum, (char, index)|
      sum +
        digits.index(char) *
        base**index
    end
  end
end
```

Now take a look at how the enums are chained:
* `.each_char`
* `.reverse_each`
* `.with_index`

This is better than:
* `.split('')`
* `.reverse`
* `.each_with_index`

First approach uses chaining of lazy enumerators while the other is first creating a new array after a split, then reversing it by creating another one, and finally creates an enumerator.

Now you know how to use 60 distinct digits (that can be represented as a char) and do the Babylonian calculations. Maybe next time we will look into converting notation between such custom systems, and try to use them together with the in-built `to_s` and `to_i` methods. Just for fun. What do you think?