I'm trying to use a bitmask to provide as many binary values as possible so that the final value will store in the limited allocate memory for a string. My current methodology is to find a maximum number and convert it to a string base-36.
value = (0 | (1<<1318)).to_s(36)
The result is 255 chars of a compressed number from which I can extract my original number of 1318. The downside is I'm limited to 1,318 binary values and I want to expand that number. Are there any alternative strategies in Ruby to compress this number even further?
You can always encode your number into base s and then represent that as string with whatever alphabet you want.
def encode(n, alphabet)
s = alphabet.size
res = []
while (n > 0)
res << n % s
n = n / s
end
res.reverse.map { |i| alphabet[i] }.join
end
Your method is then equivalent to encode(n, alphabet), where alphabet is defined as
alphabet = ((0..9).to_a + ("a".."z").to_a).join
# => "0123456789abcdefghijklmnopqrstuvwxyz"
But you might as well use all possible characters instead of only 36 of them:
extended_alphabet = (0..255).map { |i| i.chr }.join
This gives a total of (256 ** 255) possibilities, i.e. up to (2 ** 2040), which is much better than your actual (2 ** 1318).
This encoding happens to be optimal because each character of your string can have at most 256 different values, and all of them are used here.
Decoding can then be performed as follows:
def decode(encoded, alphabet)
s = alphabet.size
n = 0
decode_dict = {}; i = -1
alphabet.each_char { |c| decode_dict[c] = (i += 1) }
encoded.each_char do |c|
n = n * s + decode_dict[c]
end
n
end
If you are going to use a fixed alphabet for all your encodings, I would suggest computing the decoding dictionnary outside of the function and taking it as a parameter instead of alphabet, to avoid computing it every time you try to encode a number.
Numbers are non-negative
If the numbers are non-negative we can encode each 8-bits of each number to a character that is part of a string, and then decode the string by converting each character to 8 bits of the number.
def encode(n)
str = ''
until n.zero?
str << (n & 255).chr
n = n >> 8
end
str.reverse
end
def decode(str)
str.each_char.reduce(0) { |n,c| (n << 8) | c.ord }
end
This uses the following bit-manipulation methods in the class Integer: &, >>, << and |.
def test(n)
encoded = encode(n)
puts "#{n} => #{encoded} => #{decode(encoded)}"
end
test 1 # 1 => ["\u0001"] => 1
test 63 # 63 => ["?"] => 63
test 64 # 64 => ["#"] => 64
test 255 # 255 => ["\xFF"] => 255
test 256 # 256 => ["\u0001", "\u0000"] => 256
test 123456 # 123456 => ["\x01", "\xE2", "#"] => 123456
For example,
n = 123456
n.to_s(2)
#=> "11110001001000000"
so
n = 0b11110001001000000
#=> 123456
The bytes of this number can be visualized so:
00000001 11100010 01000000
We see that
a = [0b00000001, 0b11100010, 0b01000000]
a.map(&:chr)
#=> ["\x01", "\xE2", "#"]
Numbers can be negative
If the numbers to be encoded can be negative we need to first convert then to their absolute values then add some information to the encoded string that indicates whether they are non-negative or negative. That will require at least one additional byte so we might include a "+" for non-negative numbers and a "-" for negative numbers.
def encode(n)
sign = "+"
if n < 0
sign = "-"
n = -n
end
str = ''
until n.zero?
str << (n & 255).chr
n = n >> 8
end
(str << sign).reverse
end
def decode(str)
n = str[1..-1].each_char.reduce(0) { |n,c| (n << 8) | c.ord }
str[0] == '+' ? n : -n
end
test -255 # -255 => ["-", "\xFF"] => -255
test -256 # -256 => ["-", "\u0001", "\u0000"] => -256
test -123456 # -123456 => ["-", "\x01", "\xE2", "#"] => -123456
test 123456 # 123456 => ["+", "\x01", "\xE2", "#"] => 123456
Related
These were the instructions given on Codewars (https://www.codewars.com/kata/56b5afb4ed1f6d5fb0000991/train/ruby):
The input is a string str of digits. Cut the string into chunks (a chunk here is a substring of the initial string) of size sz (ignore the last chunk if its size is less than sz).
If a chunk represents an integer such as the sum of the cubes of its digits is divisible by 2, reverse that chunk; otherwise rotate it to the left by one position. Put together these modified chunks and return the result as a string.
If
sz is <= 0 or if str is empty return ""
sz is greater (>) than the length of str it is impossible to take a chunk of size sz hence return "".
Examples:
revrot("123456987654", 6) --> "234561876549"
revrot("123456987653", 6) --> "234561356789"
revrot("66443875", 4) --> "44668753"
revrot("66443875", 8) --> "64438756"
revrot("664438769", 8) --> "67834466"
revrot("123456779", 8) --> "23456771"
revrot("", 8) --> ""
revrot("123456779", 0) --> ""
revrot("563000655734469485", 4) --> "0365065073456944"
This was my code (in Ruby):
def revrot(str, sz)
# your code
if sz > str.length || str.empty? || sz <= 0
""
else
arr = []
while str.length >= sz
arr << str.slice!(0,sz)
end
arr.map! do |chunk|
if chunk.to_i.digits.reduce(0) {|s, n| s + n**3} % 2 == 0
chunk.reverse
else
chunk.chars.rotate.join
end
end
arr.join
end
end
It passed 13/14 test and the error I got back was as follows:
STDERR/runner/frameworks/ruby/cw-2.rb:38:in `expect': Expected: "", instead got: "095131824330999134303813797692546166281332005837243199648332767146500044" (Test::Error)
from /runner/frameworks/ruby/cw-2.rb:115:in `assert_equals'
from main.rb:26:in `testing'
from main.rb:84:in `random_tests'
from main.rb:89:in `<main>'
I'm not sure what I did wrong, I have been trying to find what it could be for over an hour. Could you help me?
I will let someone else identify the problem with you code. I merely wish to show how a solution can be speeded up. (I will not include code to deal with edge cases, such as the string being empty.)
You can make use of two observations:
the cube of an integer is odd if and only if the integer is odd; and
the sum of collection of integers is odd if and only if the number of odd integers is odd.
We therefore can write
def sum_of_cube_odd?(str)
str.each_char.count { |c| c.to_i.odd? }.odd?
end
Consider groups of 4 digits in the last example, "563000655734469485".
sum_of_cube_odd? "5630" #=> false (so reverse -> "0365")
sum_of_cube_odd? "0065" #=> true (so rotate -> "0650")
sum_of_cube_odd? "5734" #=> true (so rotate -> "7345")
sum_of_cube_odd? "4694" #=> true (so rotate -> "6944")
so we are to return "0365065073456944".
Let's create another helper.
def rotate_chars_left(str)
str[1..-1] << s[0]
end
rotate_chars_left "0065" #=> "0650"
rotate_chars_left "5734" #=> "7345"
rotate_chars_left "4694" #=> "6944"
We can now write the main method.
def revrot(str, sz)
str.gsub(/.{,#{sz}}/) do |s|
if s.size < sz
''
elsif sum_of_cube_odd?(s)
rotate_chars_left(s)
else
s.reverse
end
end
end
revrot("123456987654", 6) #=> "234561876549"
revrot("123456987653", 6) #=> "234561356789"
revrot("66443875", 4) #=> "44668753"
revrot("66443875", 8) #=> "64438756"
revrot("664438769", 8) #=> "67834466"
revrot("123456779", 8) #=> "23456771"
revrot("563000655734469485", 4) #=> "0365065073456944"
It might be slightly faster to write
require 'set'
ODD_DIGITS = ['1', '3', '5', '7', '9'].to_set
#=> #<Set: {"1", "3", "5", "7", "9"}>
def sum_of_cube_odd?(str)
str.each_char.count { |c| ODD_DIGITS.include?(c) }.odd?
end
I have this code:
1 #!/local/usr/bin/ruby
2
3 users = (1..255).to_a
4
5 x = " "
6 y = " "
7 z = " "
8 #a = " "
9
10 count = 1
11 users.each do |i|
12 x << i if count == 1
13 y << i if count == 2
14 z << i if count == 3
15 # if x.length == 60
16 # a << i if count == 1
17 # a << i if count == 2
18 # a << i if count == 3
19 # else
20 # end
21 if count == 3
22 count = 1
23 else
24 count += 1
25 end
26 end
27
28 puts x.length
29 puts y.length
30 puts z.length
31 #puts a.length
32
What this code does is append The numbers 1-255 into three different strings and outputs how many numbers are in each string.
IT WORKS
Example of working code:
[user#server ruby]$ ruby loadtest.rb
86
86
86
[user#server ruby]$
Now what I want it to do is have a failsafe called a as seen above, commented out, What I want is this, if each string contains 60 numbers I want it to append into the a string until there are no more numbers.
When I try to do it with the commented out section it outputs this:
[user#server ruby]$ ruby loadtest.rb
86
86
86
4
[user#server ruby]$ ruby loadtest.rb
WHY?! What am I doing wrong?
What this code does is append The numbers 1-255 into three different strings and outputs how many numbers are in each string.
After reducing the number of values being iterated for readability, here's what it's doing:
users = (1..5).to_a
x = " "
y = " "
z = " "
count = 1
users.each do |i|
x << i if count == 1 # => " \u0001", nil, nil, " \u0001\u0004", nil
y << i if count == 2 # => nil, " \u0002", nil, nil, " \u0002\u0005"
z << i if count == 3 # => nil, nil, " \u0003", nil, nil
if count == 3
count = 1
else
count += 1
end
end
x # => " \u0001\u0004"
y # => " \u0002\u0005"
z # => " \u0003"
puts x.length
puts y.length
puts z.length
# >> 3
# >> 3
# >> 2
Your code is creating binary inside the strings, not "numbers" as we normally think of them, as digits.
Moving on, you can clean up your logic using each_with_index and case/when. To make the results more readable I switched from accumulating into strings into arrays:
users = (1..5).to_a
x = []
y = []
z = []
users.each_with_index do |i, count|
case count % 3
when 0
x << i
when 1
y << i
when 2
z << i
end
end
x # => [1, 4]
y # => [2, 5]
z # => [3]
puts x.length
puts y.length
puts z.length
# >> 2
# >> 2
# >> 1
The real trick in this is the use of %, which does a modulo on the value.
... if each string contains 60 numbers I want it to append into the a string until there are no more numbers
As written, you are unconditionally appending to x,y,z even after they hit your limit.
You need to add a conditional around this code:
x << i if count == 1
y << i if count == 2
z << i if count == 3
so that it stops appending once it hits your limit.
By the looks of the else block that does nothing, I think you were headed in that direction:
if x.length == 60
a << i if count == 1
a << i if count == 2
a << i if count == 3
else
x << i if count == 1
y << i if count == 2
z << i if count == 3
end
Even that, though, won't do exactly what you want.
You'll want to check the string you are appending to to see if it has hit your limit yet.
I'd suggest refactoring to make it cleaner:
users.each do |i|
target_string = case count
when 1 then x
when 2 then y
when 3 then z
end
target_string = a if target_string.length == 60
target_string << i
if count == 3
count = 1
else
count += 1
end
end
It may be better to use an array instead of string as you are pushing numbers into those variables.
Let me propose a solution which achieves more or less what you are trying to do, but uses few Ruby tricks that may be useful in future.
x, y, z = r = Array.new(3) {[]}
a = []
iter = [0,1,2].cycle
(1..255).each do |i|
r.all? {|i| i.size == 60} ? a << i : r[iter.next] << i
end
p x.size, y.size, z.size
p a.size
Let's define our arrays. Even though I have arrays x, y, and z, they are there only because they were present in your code - I think we just need three arrays, each of which would collect numbers as they are picked from a range of numbers - between 1 to 255 - one by one. x,y,z = r uses parallel assignment technique and is equivalent to x,y,z = r[0],r[1],r[2]. Also, use of Array.new(3) {[]} helps in creating the Array of Array such that when we access r[1] it is initialized with empty array([]) by default.
x, y, z = r = Array.new(3) {[]}
a = []
In order to determine which array the next number picked from range has to be placed in, we will use an Enumerator generated from Enumerable#cycle. This enumerator is special - because it is soft of infinite in nature - and we can keep asking it to give an element by calling next, and it will cycle through the array elements of [0,1,2] - returning us 0,1,2,0,1,2,0,1,2... infinitely.
iter = [0,1,2].cycle
Next, we will iterate through the range of numbers 1..255. During each iteration, we will check whether all the 3 arrays in which we are collecting number have desired size of 60 with the help of Enumerable#all? - if so, we will append the number to array a - else we will assign it to one of the sub arrays of r based on the array index returned by iter enumerator.
(1..255).each do |i|
r.all? {|i| i.size == 60} ? a << i : r[iter.next] << i
end
Finally, we print the size of each of the array.
p x.size, y.size, z.size
#=> 60, 60, 60
p a.size
#=> 75
I know my code works to get the correct answer for 4 adjacent integers. But it's not working with 13.
The only thing I can think of is that it can be an issue with an unsigned int, but in Ruby I don't think I'd have that problem because it would change automatically into a Bignum class.
So that means that somewhere in my calculation I am wrong?
Please give me a hint.
# Euler 8
# http://projecteuler.net/index.php?section=problems&id=8
# Find the thirteen adjacent digits in the 1000-digit number
# that have the greatest product.
# What is the value of this product?
number = []
#split the integer as a string into an array
long_digit = "73167176531330624919225119674426574742355349194934
96983520312774506326239578318016984801869478851843
85861560789112949495459501737958331952853208805511
12540698747158523863050715693290963295227443043557
66896648950445244523161731856403098711121722383113
62229893423380308135336276614282806444486645238749
30358907296290491560440772390713810515859307960866
70172427121883998797908792274921901699720888093776
65727333001053367881220235421809751254540594752243
52584907711670556013604839586446706324415722155397
53697817977846174064955149290862569321978468622482
83972241375657056057490261407972968652414535100474
82166370484403199890008895243450658541227588666881
16427171479924442928230863465674813919123162824586
17866458359124566529476545682848912883142607690042
24219022671055626321111109370544217506941658960408
07198403850962455444362981230987879927244284909188
84580156166097919133875499200524063689912560717606
05886116467109405077541002256983155200055935729725
71636269561882670428252483600823257530420752963450"
long_digit.split("").map { |s| number << s.to_i }
#iterate through the array to find the 13 ajacent digits that have the largest product
largest_product = 0
a = 0
#stay within the bounds of the array
while number[a+12]
current_product = number[a] * number[a+1] * number[a+2] * number[a+3] * number[a+4] * number[a+5] * number[a+6] * number[a+7] * number[a+8] * number[a+9] * number[a+10] * number[a+11] * number[a+12]
if current_product > largest_product
largest_product = current_product
end
a = a + 1
end
puts largest_product
I think this solution is pretty clean and simple:
#!/usr/bin/env ruby
input = "
73167176531330624919225119674426574742355349194934
96983520312774506326239578318016984801869478851843
85861560789112949495459501737958331952853208805511
12540698747158523863050715693290963295227443043557
66896648950445244523161731856403098711121722383113
62229893423380308135336276614282806444486645238749
30358907296290491560440772390713810515859307960866
70172427121883998797908792274921901699720888093776
65727333001053367881220235421809751254540594752243
52584907711670556013604839586446706324415722155397
53697817977846174064955149290862569321978468622482
83972241375657056057490261407972968652414535100474
82166370484403199890008895243450658541227588666881
16427171479924442928230863465674813919123162824586
17866458359124566529476545682848912883142607690042
24219022671055626321111109370544217506941658960408
07198403850962455444362981230987879927244284909188
84580156166097919133875499200524063689912560717606
05886116467109405077541002256983155200055935729725
71636269561882670428252483600823257530420752963450"
.gsub(/\s+/, '')
puts input.chars
.map(&:to_i)
.each_cons(13)
.map { |seq| seq.reduce(:*) }
.max
gsub performs the trimming.
chars gets the characters.
map(&:to_i) maps all the chars to ints.
each_cons(13) gets blocks of consecutive numbers (https://ruby-doc.org/core-2.4.1/Enumerable.html#method-i-each_cons)
map { |seq| seq.reduce(:*) } is going to take each of the consecutive blocks and perform a reduce (multiplying all the numbers of each slice/consecutive block of numbers).
max gets the maximum value.
Issue seems to be due to lot of white space chars in the string long_digit that are become 0 in the array number, thus giving wrong results.
Here is a corrected and simplified version. After removing newlines and spaces using gsub, we now have a 1000 digit number and we get correct answer.
number = long_digit.gsub!(/\s/, '').split("").map{ |s| s.to_i }
n = 13
p number.each_cons(n).map{|a| a.reduce {|a, i| a = a * i }}.max
#=> 23514624000
First, let's fix the string:
long_digit.gsub!(/\s|\n/,'')
long_digit.size #=> 1000
We can speed this up by eliminating 13-character substrings that contain a zero:
shorter_digit_arr = long_digit.split('0').reject { |s| s.size < 13 }
#=> ["7316717653133",
# "6249192251196744265747423553491949349698352",
# "6326239578318",
# "18694788518438586156",
# "7891129494954595",
# "17379583319528532",
# "698747158523863",
# "435576689664895",
# "4452445231617318564",
# "987111217223831136222989342338",
# "81353362766142828",
# "64444866452387493",
# "1724271218839987979",
# "9377665727333",
# "594752243525849",
# "632441572215539753697817977846174",
# "86256932197846862248283972241375657",
# "79729686524145351",
# "6585412275886668811642717147992444292823",
# "863465674813919123162824586178664583591245665294765456828489128831426",
# "96245544436298123",
# "9878799272442849",
# "979191338754992",
# "559357297257163626956188267"]
Now, for each element of shorter_digit_arr, find the 13-character substring whose product of digits is greatest, then find the largest of those (shorter_digit_arr.size #=> 24) products. The main benefit of splitting the string into substrings in this way is that absence of zeroes allows us to perform the product calculations in a more efficient way than simply grinding out 12 multiplications for each substring:
res = shorter_digit_arr.map do |s|
cand = s[0,13].each_char.reduce(1) { |prod,t| prod * t.o_i }
best = { val: cand, offset: 0 }
(13...s.size).each do |i|
cand = cand*(s[i].to_i)/(s[i-13].to_i)
best = { val: cand, offset: i-12 } if cand > best[:val]
end
[best[:val], s[best[:offset],13]]
end.max_by(&:first)
#=> [23514624000, "5576689664895"]
puts "max_product: %d for: '%s'" % res
#=> max_product: 23514624000 for: '5576689664895'
The solution is the last 13 characters of:
s = shorter_digit_arr[7]
#=> "435576689664895"
The key here is the line:
cand = cand*(s[i].to_i)/(s[i-13].to_i)
which computes a 13-digit product by multiplying the "previous" 13-digit product by the digit added and dividing it by the digit dropped off.
In finding the maximum product for this element, the calculations are as follows:
s = "435576689664895"
cand = s[0,13].each_char.reduce(1) { |prod,t| prod * t.to_i }
#=> = "4355766896648".each_char.reduce(1) { |prod,t| prod * t.to_i }
# = 6270566400
best_val = { val: 6270566400, offset: 0 }
enum = (13...s.size).each
#=> #<Enumerator: 13...15:each>
The elements of this enumerator will be passed to the block by Enumerator#each. We can see what they are by converting enum to an array:
enum.to_a
#=> [13, 14]
We can use Enumerator#next to simulate the passing of the elements of enum to the block and their assignment to the block variable i.
Pass the first element of the enumerator (13) to the block:
i = enum.next
#=> 13
cand = cand*(s[i].to_i)/(s[i-13].to_i)
# = 6270566400*(s[13].to_i)/(s[0].to_i)
# = 6270566400*(9)/(4)
# = 14108774400
cand > best[:val]
#=> 14108774400 > 6270566400 => true
best = { val: cand, offset: i-12 }
#=> { val: 14108774400, offset: 1 }
Pass the second element (14) to the block:
i = enum.next
#=> 14
cand = cand*(s[i].to_i)/(s[i-13].to_i)
#=> = 14108774400*(s[14].to_i)/(s[1].to_i)
# = 14108774400*(5)/(3)
# = 23514624000
cand > best[:val]
#=> 23514624000 > 14108774400 => true
best = { val: 23514624000, offset: 2 }
All elements of the enumerator have now been passed to the block. We can confirm that:
i = enum.next
#=> StopIteration: iteration reached an end
The result (for shorter_digit_arr[7]) is:
[best[:val], s[best[:offset],13]]
#=> [23514624000, "435576689664895"[2,13]]
# [23514624000, "5576689664895"]
I am building a base converter. Here is my code so far:
def num_to_s(num, base)
remainders = [num]
while base <= num
num /= base #divide initial value of num
remainders << num #shovel results into array to map over for remainders
end
return remainders.map{|i| result = i % base}.reverse.to_s #map for remainders and shovel to new array
puts num_to_s(40002, 16)
end
Now it's time to account for bases over 10 where letters replace numbers. The instructions (of the exercise) suggest using a hash. Here is my hash:
conversion = {10 => 'A', 11 => 'B', 12 => 'C', 13 => 'D', 14 => 'E', 15 => 'F',}
The problem is now, how do I incorporate it so that it modifies the array? I have tried:
return remainders.map{|i| result = i % base}.map{|i| [i, i]}.flatten.merge(conversion).reverse.to_s
In an attempt to convert the 'remainders' array into a hash and merge them so the values in 'conversion' override the ones in 'remainders', but I get an 'odd list for Hash' error. After some research it seems to be due to the version of Ruby (1.8.7) I am running, and was unable to update. I also tried converting the array into a hash outside of the return:
Hashes = Hash[remainders.each {|i, i| [i, i]}].merge(conversion)
and I get an 'dynamic constant assignment' error. I have tried a bunch of different ways to do this... Can a hash even be used to modify an array? I was also thinking maybe I could accomplish this by using a conditional statement within an enumerator (each? map?) but haven't been able to make that work. CAN one put a conditional inside an enumerator?
Yes, you could use a hash:
def digit_hash(base)
digit = {}
(0...[10,base].min).each { |i| digit.update({ i=>i.to_s }) }
if base > 10
s = ('A'.ord-1).chr
(10...base).each { |i| digit.update({ i=>s=s.next }) }
end
digit
end
digit_hash(40)
#=> { 0=>"0", 1=>"1", 2=>"2", 3=>"3", 4=>"4",
# 5=>"5", 6=>"6", 7=>"7", 8=>"8", 9=>"9",
# 10=>"A", 11=>"B", 12=>"C", ..., 34=>"Y", 35=>"Z",
# 36=>"AA", 37=>"AB", 38=>"AC", 39=>"AD" }
There is a problem in displaying digits after 'Z'. Suppose, for example, the base were 65. Then one would not know if "ABC" was 10-11-12, 37-12 or 10-64. That's detail we needn't worry about.
For variety, I've done the base conversion from high to low, as one might do with paper and pencil for base 10:
def num_to_s(num, base)
digit = digit_hash(base)
str = ''
fac = base**((0..Float::INFINITY).find { |i| base**i > num } - 1)
until fac.zero?
d = num/fac
str << digit[d]
num -= d*fac
fac /= base
end
str
end
Let's try it:
num_to_s(134562,10) #=> "134562"
num_to_s(134562, 2) #=> "100000110110100010"
num_to_s(134562, 8) #=> "406642"
num_to_s(134562,16) #=> "20DA2"
num_to_s(134562,36) #=> "2VTU"
Let's check the last one:
digit_inv = digit_hash(36).invert
digit_inv["2"] #=> 2
digit_inv["V"] #=> 31
digit_inv["T"] #=> 29
digit_inv["U"] #=> 30
So
36*36*36*digit_inv["2"] + 36*36*digit_inv["V"] +
36*digit_inv["T"] + digit_inv["U"]
#=> 36*36*36*2 + 36*36*31 + 36*29 + 30
#=> 134562
The expression:
(0..Float::INFINITY).find { |i| base**i > num }
computes the smallest integer i such that base**i > num. Suppose, for example,
base = 10
num = 12345
then i is found to equal 5 (10**5 = 100_000). We then raise base to this number less one to get the initial factor:
fac = base**(5-1) #=> 10000
Then the first (base-10) digit is
d = num/fac #=> 1
the remainder is
num -= d*fac #=> 12345 - 1*10000 => 2345
and the factor for the next digit is:
fac /= base #=> 10000/10 => 1000
I made a couple of changes from my initial answer to make it 1.87-friedly (I removed Enumerator#with_object and Integer#times), but I haven't tested with 1.8.7, as I don't have that version installed. Let me know if there are any problems.
Apart from question, you can use Fixnum#to_s(base) to convert base.
255.to_s(16) # 'ff'
I would do a
def get_symbol_in_base(blah)
if blah < 10
return blah
else
return (blah - 10 + 65).chr
end
end
and after that do something like:
remainders << get_symbol_in_base(num)
return remainders.reverse.to_s
I'm working in Ruby with an array that contains a series of numbers in human-readable format (e.g., 2.5B, 1.27M, 600,000, where "B" stands for billion, "M" stands for million). I'm trying to convert all elements of the array to the same format.
Here is the code I've written:
array.each do |elem|
if elem.include? 'B'
elem.slice! "B"
elem = elem.to_f
elem = (elem * 1000000000)
else if elem.include? 'M'
elem.slice! "M"
elem = elem.to_f
elem = (elem * 1000000)
end
end
When I inspect the elements of the array using puts(array), however, the numbers appear with the "B" and "M" sliced off but the multiplication conversion does not appear to have been applied (e.g., the numbers now read 2.5, 1.27, 600,000, instead of 2500000000, 1270000, 600,000).
What am I doing wrong?
First thing to note is that else if in ruby is elsif. See http://www.tutorialspoint.com/ruby/ruby_if_else.htm
Here is a working function for you to try out:
def convert_array_items_from_human_to_integers(array)
array.each_with_index do |elem,i|
if elem.include? 'B'
elem.slice! "B"
elem = elem.to_f
elem = (elem * 1000000000)
elsif elem.include? 'M'
elem.slice! "M"
elem = elem.to_f
elem = (elem * 1000000)
end
array[i] = elem
end
return array
end
Calling convert_array_items_from_human_to_integers(["2.5B", "1.2M"])
returns [2500000000.0, 1200000.0]
Another variation:
array = ['2.5B', '1.27M', '$600000']
p array.each_with_object([]) { |i, a|
i = i.gsub('$', '')
a << if i.include? 'B'
i.to_f * 1E9
elsif i.include? 'M'
i.to_f * 1E6
else
i.to_f
end
}
#=> [2500000000.0, 1270000.0, 600000.0]
Try this:
array.map do |elem|
elem = elem.gsub('$','')
if elem.include? 'B'
elem.to_f * 1000000000
elsif elem.include? 'M'
elem.to_f * 1000000
else
elem.to_f
end
end
This uses map instead of each to return a new array. Your attempt assigns copies of the array elements, leaving the original array in place (except for the slice!, which modifies in place). You can dispense with the slicing in the first place, since to_f will simply ignore any non-numeric characters.
EDIT:
If you have leading characters such as $2.5B, as your question title indicates (but not your example), you'll need to strip those explicitly. But your sample code doesn't handle those either, so I assume that's not an issue.
Expanding a bit on pjs' answer:
array.each do |elem|
elem is a local variable pointing to each array element, one at a time. When you do this:
elem.slice! "B"
you are sending a message to that array element telling it to slice the B. And you're seeing that in the end result. But when you do this:
elem = elem.to_f
now you've reassigned your local variable elem to something completely new. You haven't reassigned what's in the array, just what elem is.
Here's how I'd go about it:
ARY = %w[2.5B 1.27M 600,000]
def clean_number(s)
s.gsub(/[^\d.]+/, '')
end
ARY.map{ |v|
case v
when /b$/i
clean_number(v).to_f * 1_000_000_000
when /m$/i
clean_number(v).to_f * 1_000_000
else
clean_number(v).to_f
end
}
# => [2500000000.0, 1270000.0, 600000.0]
The guts of the code are in the case statement. A simple check for the multiplier allows me to strip the undesired characters and multiply by the right value.
Normally we could use to_f to find the floating-point number to be multiplied for strings like '1.2', but it breaks down for things like '$1.2M' because of the "$". The same thing is true for embedded commas marking thousands:
'$1.2M'.to_f # => 0.0
'1.2M'.to_f # => 1.2
'6,000'.to_f # => 6.0
'6000'.to_f # => 6000.0
To fix the problem for simple strings containing just the value, it's not necessary to do anything fancier than stripping undesirable characters using gsub(/[^\d.]+/, ''):
'$1.2M'.gsub(/[^\d.]+/, '') # => "1.2"
'1.2M'.gsub(/[^\d.]+/, '') # => "1.2"
'6,000'.gsub(/[^\d.]+/, '') # => "6000"
'6000'.gsub(/[^\d.]+/, '') # => "6000"
[^\d.] means "anything NOT a digit or '.'.
Be careful how you convert your decimal values to integers. You could end up throwing away important precision:
'0.2M'.gsub(/[^\d.]+/, '').to_f * 1_000_000 # => 200000.0
'0.2M'.gsub(/[^\d.]+/, '').to_i * 1_000_000 # => 0
('0.2M'.gsub(/[^\d.]+/, '').to_f * 1_000_000).to_i # => 200000
Of course all this breaks down if your string is more complex than a simple number and multiplier. It's easy to break down a string and identify those sort of sub-strings, but that's a different question.
I would do it like this:
Code
T, M, B = 1_000, 1_000_000, 1_000_000_000
def convert(arr)
arr.map do |n|
m = n.gsub(/[^\d.TMB]/,'')
m.to_f * (m[-1][/[TMB]/] ? Object.const_get(m[-1]) : 1)
end
end
Example
arr = %w[$2.5B 1.27M 22.5T, 600,000]
convert(arr)
# => [2500000000.0, 1270000.0, 22500.0, 600000.0]
Explanation
The line
m = n.gsub(/[^\d.TMB]/,'')
# => ["2.5B", "1.27M", "22.5T", "600000"]
merely eliminates unwanted characters.
m.to_f * (m[-1][/[TMB]/] ? Object.const_get(m[-1]) : 1)
returns the product of the string converted to a float and a constant given by the last character of the string, if that character is T, M or B, else 1.
Actual implementation might be like this:
class A
T, M, B = 1_000, 1_000_000, 1_000_000_000
def doit(arr)
c = self.class.constants.map(&:to_s).join
arr.map do |n|
m = n.gsub(/[^\d.#{c}]/,'')
m.to_f * (m[-1][/[#{c}]/] ? self.class.const_get(m[-1]) : 1)
end
end
end
If we wished to change the reference for 1,000 from T to K and add T for trillion, we would need only change
T, M, B = 1_000, 1_000_000, 1_000_000_000
to
K, M, B, T = 1_000, 1_000_000, 1_000_000_000, 1_000_000_000_000