2
\$\begingroup\$

I wrote a simple quadratic equation solver with Ruby to help me solve quadratic equations. Please tell me what I did right or wrong, and what I can do to improve it.

#!/usr/bin/ruby

if ARGV.length != 3
        STDERR << "Usage: #{$0} <a> <b> <c>\n"
        exit 1
end

a = ARGV[0].to_i
b = ARGV[1].to_i
c = ARGV[2].to_i

disc = b**2 - 4*a*c
d_str = (disc >= 0) ? (Math.sqrt(disc) % 1 == 0) ? Math.sqrt(disc).to_i : "√#{disc}" : "√#{disc}"

s = "#{-b} ± #{d_str}"
d = "#{2*a}"
puts "\e[4m#{s}\e[0m"
#puts "─"*(s.length)
puts d.center s.length
\$\endgroup\$
2
  • \$\begingroup\$ I would show the calculated value Math.sqrt(disc) even when it is a floating point number. Its also a good idea to check before using terminal control codes (A simple, though not foolproof, way is to check $stdout.isatty). The colorize gem is also, though ti does add a dependency to your code. \$\endgroup\$ Commented Mar 2, 2017 at 17:29
  • \$\begingroup\$ Any updates on this? \$\endgroup\$
    – anna328p
    Commented Mar 4, 2017 at 7:54

1 Answer 1

4
\$\begingroup\$

What you did right is solve the problem.

What you did "wrong" really depends on a number of factors, so I'll respond with what you can do to improve it, with an example that implements those improvements. Note, many improvements will make the code longer.

Improvements

  • Use the shebang #!/usr/bin/env ruby. It allows the environment to have ruby in a different directory than /usr/bin/ruby.
  • Use $stderr.puts "Usage: #{$0} <a> <b> <c>". $stderr is a global variable and can be changed. It allows standard error to be redirected, say to a file or a logger. It would be restored by $stderr = STDERR.
  • Ruby indentation standard is 2 spaces and no hard tabs.
  • Use disc_str to indicate it is a String version of disc.
  • Use disc_sqrt_str to indicate it is a String version of disc_sqrt.
  • Use a variable to reduce wasted calculations; sqrt is not a simple calculation.
  • Nested ? operators can be hard to read and can lead to mistakes.
  • Use numerator and denominator to convey intention, instead of s and d.
  • Allow for the case when discriminant is zero.
  • Use parentheses for arguments when method calls are arguments. Thus instead of f g h x do f g(h(x)) or even better f(g(h(x))).
  • Use discriminant because it is a Mathematics domain term. A non-mathematics literate developer will be blocked from maintaining it because Googling "disc" won't help them.
  • Eliminate common factors.
  • Eliminate fraction if denominator is 1.
  • Define methods for greater flexibility.
  • Include some tests to test each pathway.
  • Use appropriate comments and self-commenting code by using good names.
  • Use Rubocop to provide similar automated improvements.
  • Use the colorize gem in an optional way, since it is not actually necessary.
  • Check if it is available by using ''.respond_to?(:underline), which will mean that it is more flexible because another method could be added to String which might do a similar thing or even something rather different; CSS, LaTeX, ASCII, ... .
  • Use Unicode codes instead of the characters directly. Some editors may not display Unicode characters properly and could change the characters to something unexpected.
#!/usr/bin/env ruby
#
#   quadratic   - Output quadratic solutions as a fraction
#
#     Luis Esteban    9 August 2020
#       review of code of Dmitry Kudriavtsev

# Output rational solutions to the quadratic equation
#   For a.x^2 + b.x + c = 0
#
#   x = (-b ± √d) / 2a
#   d = b^2 - 4.a.c       (discriminant)
#

begin
  require 'colorize'
rescue LoadError
end

ALLOW_IMAGINARY = true

def solve_quadratic(a, b, c)
  discriminant = b**2 - 4*a*c
  denominator  = 2 * a

  if discriminant > 0
    discriminant_sqrt = Math.sqrt(discriminant)
    if discriminant_sqrt % 1 == 0
      b, discriminant, denominator = simplify(b, discriminant_sqrt.to_i, denominator)
      numerator = [-b, " \u00b1 ", discriminant]
    else
      b, discriminant, denominator = simplify_with_sqrt(b, discriminant, denominator)
      numerator = [-b, " \u00b1 \u221a", discriminant]
    end
  elsif discriminant == 0
    solution    = Rational(-b, denominator)
    numerator   = [solution.numerator]
    denominator = solution.denominator
  else
    if ALLOW_IMAGINARY
      discriminant_sqrt = Math.sqrt(-discriminant)
      discriminant_sqrt = discriminant_sqrt.to_i if discriminant_sqrt % 1 == 0
      if discriminant_sqrt % 1 == 0
        b, discriminant, denominator = simplify(b, discriminant_sqrt.to_i, denominator)
        numerator = [-b, " \u00b1 i \u2a2f ", discriminant]
      else
        b, discriminant, denominator = simplify_with_sqrt(b, discriminant, denominator)
        numerator = [-b, " \u00b1 i \u2a2f \u221a", -discriminant]
      end
    else
      numerator = []
    end
  end
  
  [numerator, denominator]
end

def simplify(b, discriminant, denominator)
  gcd = [b, discriminant, denominator].inject(&:gcd)
  gcd = -gcd unless denominator.positive?
  
  [b, discriminant, denominator].map{|n| n / gcd }
end

def simplify_with_sqrt(b, discriminant, denominator)
  gcd = [b**2, discriminant, denominator**2].inject(&:gcd)
  gcd_sqrt = Math.sqrt(gcd).round
  if denominator.negative?
    gcd      = -gcd
    gcd_sqrt = -gcd_sqrt
  end
  
  [
    b            / gcd_sqrt,
    discriminant / gcd,
    denominator  / gcd_sqrt
  ]
end

def display_fraction(numerator, denominator)
  size        = numerator.size
  numerator   = numerator.join
  denominator = denominator.to_s
  width       = [numerator, denominator].map(&:length).max + 2

  if size > 0
    if denominator != "1"
      if ''.respond_to?(:underline)
        puts "#{numerator}".center(width).underline
        puts denominator.center(width)
      else
        puts numerator.center(width)
        puts "─" * width
        puts denominator.center(width)
      end
    else
      puts numerator
    end
  else
    puts "No solutions"
  end
end

if ARGV.length != 3
  $stderr.puts "Usage: #{$0} <a> <b> <c>"
  exit 1
else
  a, b, c = ARGV.map(&:to_i)
  puts "solution(s) to #{a}x\u00b2 + #{b}x + #{c} = 0"
  
  display_fraction(*solve_quadratic(a,b,c))
end

# Testing
#  [
#    [1, 1, -12],    # Integer discriminant
#    [-1, 1, 12],    # Make denominator positive
#    [1, 2, 0],      # Don't show denominator if 1
#    [4, 4, 1],      # Don't show discriminant if 0
#    [1, 2, -17],    # Don't show denominator if 1 with surd
#    [2, 4, 2],      # Don't show denominator if 1 and  discriminant if 0
#    [1, 1, -13],    # Show surd
#    [1, 2, 17],     # Imaginary solution with integer discriminant no denominator
#    [1, 2, 16],     # Imaginary solution with correct cancellation of surd
#    [2, 0, 2],      # Imaginary solution with surd, hide denominator
#    [2, 8, 16],     # Imaginary solution with integer discriminant
#  ].each do |a,b,c|
#    puts "For: a = #{a.inspect}, b = #{b.inspect}, c = #{c.inspect}"
#    puts
#    display_fraction(*solve_quadratic(a,b,c))
#    puts
#  end
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.