DEV Community

Jakub Godawa
Jakub Godawa

Posted on

How to make a custom positional notation system in Ruby 🧮

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']
  .map(&:to_a)
  .flatten
  .size
=> 36
irb(main)> 189.to_s(17)
=> "b2"
irb(main)> 35.to_s(36)
=> "z"
Enter fullscreen mode Exit fullscreen mode

We can also play the other way around:

irb(main)> "1001110".to_i(2)
=> 78
irb(main)> "z".to_i(36)
=> 35
Enter fullscreen mode Exit fullscreen mode

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!

Babylonian numeral system

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 = 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
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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?

Top comments (0)