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"
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!
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
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
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.