--Apple-Mail-4-67905564 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=US-ASCII; delsp=yes; format=flowed Begin forwarded message: > From: Paul Vaillant <paul.vaillant / gmail.com> > Date: September 12, 2005 1:55:13 PM CDT > To: submission / rubyquiz.com > Subject: Please Forward: Ruby Quiz Submission > Reply-To: paul.vaillant / gmail.com > > > Not using CommandLine because I couldn't find it; uses OptionParser > instead. > > paul --Apple-Mail-4-67905564 Content-Transfer-Encoding: 7bit Content-Type: application/octet-stream; x-unix-mode=0666; name="ndiff.rb" Content-Disposition: attachment; filename=ndiff.rb #!/usr/bin/ruby ## Proposed solution to http://www.rubyquiz.org/quiz046.html ## Written by Paul Vaillant (paul.vaillant / gmail.com) ## Permission granted to do whatever you'd like with this code require 'optparse' require 'ostruct' class DataFile @@options = OpenStruct.new({:digits => 0, :tol => 0.0, :quiet => false, :stats => false}) def self.options return @@options end @@opt_parser = OptionParser.new {|opts| opts.banner = "Usage: ndiff [options] file1 file2" opts.separator "" opts.separator "Numerically compare files line by line, numerical field by numerical field." opts.separator "" opts.on("-d", "--digits INT", Integer, "Maximum number of significant digits that should match. (default: 0)") {|d| DataFile.options.digits = d } opts.on("-h", "--help", "Output this help.") { puts opts exit } opts.on("-q", "--quiet", "No output, just exit code.") { DataFile.options.quiet = true } opts.on("-s", "--statistics", "Provide comparison statistics only.") { DataFile.options.stats = true } opts.on("-t", "--tolerance DBL", Float, "Tolerate <= DBL distance between numbers. (default: 0.0)") {|dbl| DataFile.options.tol = dbl } } def self.parse_options(args) @@opt_parser.parse!(args) end def self.help puts @@opt_parser end attr_reader :filename, :lines, :data def initialize(filename) @filename = filename @lines = Array.new @data = Array.new parse_file(filename) end def parse_file(file) raise "'#{filename}' does not exist" unless FileTest.exists?(filename) raise "'#{filename}' isn't readable" unless FileTest.readable?(filename) File.read(filename).each_line {|l| l.chomp! @lines << l @data << parse_line(l.strip) } end def parse_line(line) if line =~ /^-?\d+$/ ## this is a line with a single integer on it ## ex. 3 return line.to_i elsif line =~ /^-?\d+[eE][-+]?\d+$/ ## this is a line with a single integer on it with exponent ## ex. 3e+01 num,exp = line.split(/[eE]/) return (num.to_i)*(10**exp.to_i) elsif line =~ /^-?\d+\.\d+$/ ## this is a line with a single float on it ## ex. 0.00323 return line.to_f elsif line =~ /^-?\d+\.\d+[eE][-+]?\d+$/ ## this is a line with a single float on it with exponent ## ex. 3.23E-02 num,exp = line.split(/[eE]/) return (num.to_f)*(10**exp.to_i) else ## this must have several number on it ## ex. Cy=0.11278889E-01 Cx=-1.343e+02 numbers = line.split(/\s+/).collect {|entry| name,number = entry.split(/=/) [name, parse_line(number)] } end end class CompareResults attr_reader :diffs def initialize() @diffs = Array.new @count = 0 @ranges = Array.new end def diff(line, x1, x2) @diffs << [line, x1, x2] end def stats max_range = @ranges.max avg_dist = @ranges.inject(0) {|i,s| s + i} / @ranges.size mean_dist = @ranges.sort[(@ranges.size/2).to_i-1] buf = '' buf << "Numbers compared: #{@count}\n" buf << "Distance range: 0.0..#{max_range}\n" buf << "Average distance: #{avg_dist} [guess]\n" buf << "Mean distance: #{mean_dist} [guess]\n" return buf end def to_s buf = '' start_line = nil last_line = nil buf1 = '' buf2 = '' @diffs.each {|line,x1,x2| if start_line && (last_line + 1 != line) if start_line != last_line buf << "#{start_line},#{last_line}c#{start_line},#{last_line}" else buf << "#{start_line}c#{start_line}" end buf << "\n" << buf1 << "---\n" << buf2 start_line = nil end start_line = line unless start_line last_line = line buf1 << "< " << x1 << "\n" buf2 << "> " << x2 << "\n" } if start_line if start_line != last_line buf << "#{start_line},#{last_line}c#{start_line},#{last_line}" else buf << "#{start_line}c#{start_line}" end buf << "\n" << buf1 << "---\n" << buf2 start_line = nil end return buf end def compare(line1, line2) ## check if line1 or line2 is an array and both are the same size if Array === line1 && Array === line2 && line1.size == line2.size ## each portion of the array must match line1.each_with_index {|n, i| ret = compare(n, line2[i]) return ret if ret } return false elsif Array === line1 || Array === line2 ## automatic difference; compound line vs non-compound line or compound line size mismatch ## TBI show should this be counted in the stats? return true else @count += 1 ## no difference if they match exactly return false if line1 == line2 ## check against digits and tol digit_check = false if DataFile.options.digits > 0 sd1 = significant_digits(line1, DataFile.options.digits) sd2 = significant_digits(line2, DataFile.options.digits) digit_check = (sd1 == sd2) end return false if digit_check tol_check = false if DataFile.options.tol > 0 tol_check = (((line1 / DataFile.options.tol).to_i - (line2 / DataFile.options.tol).to_i).abs <= 1) end return false if tol_check ## there is a difference! @ranges << (line1 - line2).abs return true end end ## creates an integer of all the significant digits of n, limited to x if > 0 def significant_digits(n, x = 0) raise "cannot generate unlimited significant digits; x must be > 0" unless x > 0 while n.abs >= 1 n = n / 10.0 end while n.abs < 0.1 n = n * 10 end ## now n is < 1 && >= 0.1 i = 0 while x > 0 n = n * 10 j = n.to_i n = n % j i = (i == 0 ? j : i*10+j) x = x - 1 end ## rounding is implemented because it would be required for ## 2.00 to match 1.99 with digits == 1 ## (as given in the description of how this should work) i = i + 1 if (n * 10).to_i >= 5 return i end end def compare(other) results = CompareResults.new @data.each_with_index {|line, index| ret = results.compare(line, other.data[index]) results.diff(index+1, @lines[index], other.lines[index]) if ret } return results end end DataFile.parse_options(ARGV) unless ARGV.size == 2 DataFile.help exit -1 end results = DataFile.new(ARGV[0]).compare(DataFile.new(ARGV[1])) if DataFile.options.stats puts results.stats elsif !DataFile.options.quiet puts results.to_s unless results.diffs.empty? end exit (results.diffs.empty? ? 0 : 1) --Apple-Mail-4-67905564 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=US-ASCII; format=flowed --Apple-Mail-4-67905564--