patternrubyMinor
Curious fractions in functional Ruby
Viewed 0 times
functionalcuriousfractionsruby
Problem
Problem 33 in Project Euler asks:
The fraction 49/98 is a curious fraction, as an inexperienced
mathematician in attempting to simplify it may incorrectly believe
that 49/98 = 4/8, which is correct, is obtained by canceling the 9s.
We shall consider fractions like, 30/50 = 3/5, to be trivial examples.
There are exactly four non-trivial examples of this type of fraction,
less than one in value, and containing two digits in the numerator and
denominator.
If the product of these four fractions is given in its lowest common
terms, find the value of the denominator.
This is the code I came up with, which solves the problem pretty handily. But, I'm concerned that this is messy and unreadable, even with the comments - and I simply hate using magic numbers/indices littered throughout the code. Also, I do not want a procedural ("C" style) solution - I'm learning how to write idiomatic (read "functional") programming with Ruby, and I'd like to learn how to write cleaner functional code.
```
a = 10.upto(99).to_a.map(&:to_s) # Create array of integers 10 - 99
puts a.product(a) # Form cartesion product [a,b] for all 2 digit numbers a,b
.reject{|x| x.any? {|y| y.chars.include? '0'}} # Numbers inlcuding '0' are the trivial matches
.reject{|x| x.first == x.last || # Reject pairs where a = b
x.first.to_f/x.last.to_f > 1 || # Reject fractions greater than 1
(x.first.chars & x.last.chars).size != 1} # Reject pairs in which no digits are common
.map{|x| [x.first.to_f / x.last.to_f, x.first.chars, x.last.chars, x.first.chars & x.last.chars]} # Ex: [0.5, ["4", "9"], ["9", "8"], ["9"]]
.map{|x| [x.first, x[1] - x.last, x[2] - x.last, x[1], x[2]]} # Ex: [0.5, ["4"], ["8"], ["4", "9"], ["9", "8"]]
.select{|x| x.length == 5 && x.first == x[1].join.to_f/x[2].join.to_f} # Select only the tuples which match the condition
.map{|x| Rational(x[
The fraction 49/98 is a curious fraction, as an inexperienced
mathematician in attempting to simplify it may incorrectly believe
that 49/98 = 4/8, which is correct, is obtained by canceling the 9s.
We shall consider fractions like, 30/50 = 3/5, to be trivial examples.
There are exactly four non-trivial examples of this type of fraction,
less than one in value, and containing two digits in the numerator and
denominator.
If the product of these four fractions is given in its lowest common
terms, find the value of the denominator.
This is the code I came up with, which solves the problem pretty handily. But, I'm concerned that this is messy and unreadable, even with the comments - and I simply hate using magic numbers/indices littered throughout the code. Also, I do not want a procedural ("C" style) solution - I'm learning how to write idiomatic (read "functional") programming with Ruby, and I'd like to learn how to write cleaner functional code.
```
a = 10.upto(99).to_a.map(&:to_s) # Create array of integers 10 - 99
puts a.product(a) # Form cartesion product [a,b] for all 2 digit numbers a,b
.reject{|x| x.any? {|y| y.chars.include? '0'}} # Numbers inlcuding '0' are the trivial matches
.reject{|x| x.first == x.last || # Reject pairs where a = b
x.first.to_f/x.last.to_f > 1 || # Reject fractions greater than 1
(x.first.chars & x.last.chars).size != 1} # Reject pairs in which no digits are common
.map{|x| [x.first.to_f / x.last.to_f, x.first.chars, x.last.chars, x.first.chars & x.last.chars]} # Ex: [0.5, ["4", "9"], ["9", "8"], ["9"]]
.map{|x| [x.first, x[1] - x.last, x[2] - x.last, x[1], x[2]]} # Ex: [0.5, ["4"], ["8"], ["4", "9"], ["9", "8"]]
.select{|x| x.length == 5 && x.first == x[1].join.to_f/x[2].join.to_f} # Select only the tuples which match the condition
.map{|x| Rational(x[
Solution
Array multiple assignment
You pass arrays around, and you find yourself calling for
Ruby allows you to implicitely assign elements of the array to parameters in your block according to its location, so you could write the same "maps" like this:
Note that unneeded array elements at the end of the array are truncated, so you don't have to write them. For the unneeded elements at the beginning of the array I've used the
If it deserves a comment - it deserves a method
Instead of commenting your code, consider refactoring it into methods. Each method will do one thing, and, given the proper name, will be descriptive when used, and easily read in itself:
Make your steps logical
The last few steps you take look like they are held out with gum, and that you made some acrobatics to stay one-line and "idiomatic". It is better to make your methods do something you can explain, than simply adding and removing temp-variables:
Update
Yes, it seems that using
I can suggest a few strategies where you don't have to fall back to using
-
Use multiple assignment in the first line of your method:
-
Use a hash to hold your values, and use keyword arguments in your method
You pass arrays around, and you find yourself calling for
x[1] and x.last - this is very unreadable, and brittle.Ruby allows you to implicitely assign elements of the array to parameters in your block according to its location, so you could write the same "maps" like this:
.map do|numerator, denominator|
[numerator.to_f / denominator.to_f, numerator.chars, denominator.chars,
numerator.chars & denominator.chars]
end.map do |result, numerator_digits, denominator_digits, mutual_digit|
[result, numerator_digits - mutual_digit, denominator_digits - mutual_digit,
numerator_digits, denominator_digits]
end.select do|result, new_numerator_digits, new_denominator_digits|
result == new_numerator_digits.join.to_f/new_denominator_digits.join.to_f
end.map do |_, _, _, numerator_digits, denominator_digits|
Rational(numerator_digits.join.to_i, denominator_digits.join.to_i)
end.reduce(:*)Note that unneeded array elements at the end of the array are truncated, so you don't have to write them. For the unneeded elements at the beginning of the array I've used the
_ idiom which tells the method that I expect these parameters, but I won't need them.If it deserves a comment - it deserves a method
Instead of commenting your code, consider refactoring it into methods. Each method will do one thing, and, given the proper name, will be descriptive when used, and easily read in itself:
def trivial?(pair)
pair.any? {|y| y.chars.include? '0'}
end
def identical?(numerator, denominator)
numerator == denominator
end
def larger_than_one?(numerator, denominator)
numerator.to_i > numerator.to_i
end
def has_common_digits?(numerator, denominator)
(numerator.chars & numerator.chars).size > 0
end
a = 10.upto(99).to_a.map(&:to_s)
puts a.product(a)
.reject(&method(:trivial?))
.reject(&method(:identical?))
.reject(&method(:larger_than_one?))
.select(&method(:has_common_digits?))Make your steps logical
The last few steps you take look like they are held out with gum, and that you made some acrobatics to stay one-line and "idiomatic". It is better to make your methods do something you can explain, than simply adding and removing temp-variables:
def reduced_fraction(numerator, denominator)
mutual_digit = numerator.chars & denominator.chars
(numerator.chars - mutual_digit).join.to_f / (denominator.chars - mutual_digit).join.to_f
end
a = 10.upto(99).to_a.map(&:to_s)
puts a.product(a)
.reject(&method(:trivial?))
.reject(&method(:identical?))
.reject(&method(:larger_than_one?))
.select(&method(:has_common_digits?))
.select do |numerator, denominator|
numerator.to_f / denominator.to_f == reduced_fraction(numerator, denominator)
end.map { |fraction| Rational(*fraction) }
.reduce(:*)Update
Yes, it seems that using
&method(:something) in map and multiple assignment are mutually exclusive... :(I can suggest a few strategies where you don't have to fall back to using
pair.first, pair.last:-
Use multiple assignment in the first line of your method:
def identical?(pair)
numerator, denominator = pair
...
end-
Use a hash to hold your values, and use keyword arguments in your method
def identical?(numerator: nil, denominator: nil)
numerator == denominator
end
a = 10.upto(99).to_a.map(&:to_s)
puts a.product(a)
.reject(&method(:trivial?))
.map { |numerator, denominator| {numerator: numerator, denominator: denominator } }
.reject(&method(:identical?))Code Snippets
.map do|numerator, denominator|
[numerator.to_f / denominator.to_f, numerator.chars, denominator.chars,
numerator.chars & denominator.chars]
end.map do |result, numerator_digits, denominator_digits, mutual_digit|
[result, numerator_digits - mutual_digit, denominator_digits - mutual_digit,
numerator_digits, denominator_digits]
end.select do|result, new_numerator_digits, new_denominator_digits|
result == new_numerator_digits.join.to_f/new_denominator_digits.join.to_f
end.map do |_, _, _, numerator_digits, denominator_digits|
Rational(numerator_digits.join.to_i, denominator_digits.join.to_i)
end.reduce(:*)def trivial?(pair)
pair.any? {|y| y.chars.include? '0'}
end
def identical?(numerator, denominator)
numerator == denominator
end
def larger_than_one?(numerator, denominator)
numerator.to_i > numerator.to_i
end
def has_common_digits?(numerator, denominator)
(numerator.chars & numerator.chars).size > 0
end
a = 10.upto(99).to_a.map(&:to_s)
puts a.product(a)
.reject(&method(:trivial?))
.reject(&method(:identical?))
.reject(&method(:larger_than_one?))
.select(&method(:has_common_digits?))def reduced_fraction(numerator, denominator)
mutual_digit = numerator.chars & denominator.chars
(numerator.chars - mutual_digit).join.to_f / (denominator.chars - mutual_digit).join.to_f
end
a = 10.upto(99).to_a.map(&:to_s)
puts a.product(a)
.reject(&method(:trivial?))
.reject(&method(:identical?))
.reject(&method(:larger_than_one?))
.select(&method(:has_common_digits?))
.select do |numerator, denominator|
numerator.to_f / denominator.to_f == reduced_fraction(numerator, denominator)
end.map { |fraction| Rational(*fraction) }
.reduce(:*)def identical?(pair)
numerator, denominator = pair
...
enddef identical?(numerator: nil, denominator: nil)
numerator == denominator
end
a = 10.upto(99).to_a.map(&:to_s)
puts a.product(a)
.reject(&method(:trivial?))
.map { |numerator, denominator| {numerator: numerator, denominator: denominator } }
.reject(&method(:identical?))Context
StackExchange Code Review Q#48256, answer score: 3
Revisions (0)
No revisions yet.