Custom positional notation system

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:

irb(main)> ['0'..'9', 'a'..'z']
=> 36
irb(main)> 189.to_s(17)
=> "b2"
irb(main)> 35.to_s(36)
=> "z"

We can also play the other way around:

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 this is a real issue!

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:

require 'rspec'

PositionalNotation =

RSpec.describe PositionalNotation do
  subject(:b3) do

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

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

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

class PositionalNotation
  attr_reader :digits, :base

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

  def call(number)
      .reduce(0) do |sum, (char, index)|
      sum +
        digits.index(char) *

Take a look at how the enums are chained:

  • .each_char
  • .reverse_each
  • .with_index

This is better than:

  • .split('')
  • .reverse
  • .each_with_index

The 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. In the next posts we will look into converting notation between such custom systems, and maybe try to use them together with the in-built to_s and to_i methods.

Leave a Reply

Your email address will not be published. Required fields are marked *