Numbers

 
   

Ruby Way
By Hal Fulton
Slots : 1.0
Table of Contents
 


On two occasions I have been asked [by members of Parliament], 'Pray, Mr. Babbage, if you put into the machine wrong figures, will the right answers come out?' I am not able rightly to apprehend the kind of confusion of ideas that could provoke such a question.

Charles Babbage

Numeric data is the original data type, the native language of the computer. We would be hard-pressed to find areas of our experience in which numbers aren't applicable. It doesn't matter whether you're an accountant or an aeronautical engineer; you can't survive without numbers. We present here a few ways to process, manipulate, convert, and analyze numeric data.

Performing Bit-level Operations on Numbers

Occasionally, we might need to operate on a Fixnum as a binary entity. This is less common in application level programming, but the need still arises.

Ruby has a relatively full set of capabilities in this area. For convenience, numeric constants can be expressed in binary, octal, or hexadecimal. The usual operators AND, OR, XOR, and NOT are expressed by the Ruby operators &, |, ^, and ~, respectively.

 

 x = 0377           # Octal  (decimal 255) y = 0b00100110     # Binary (decimal  38) z = 0xBEEF         # Hex    (decimal 48879) a = x | z          # 48895 (bitwise OR) b = x & z          #   239 (bitwise AND) c = x ^ z          # 48656 (bitwise XOR) d = ~ y            #   -39 (negation or 1's complement) 

The instance method size can be used to determine the wordsize of the specific architecture on which the program is running.

 

 bytes = 1.size     # Returns 4 for one particular machine 

There are left-shift and right-shift operators (<< and >>, respectively). These are logical shift operations; they don't disturb the sign bit (although >> does propagate it).

 

 x = 8 y = -8 a = x >> 2         # 2 b = y >> 2         # -2 c = x << 2         # 32 d = y << 2         # -32 

Of course, anything shifted far enough to result in a zero value will lose the sign bit because -0 is merely 0.

Brackets can be used to treat numbers as arrays of bits. The 0th bit is the least significant bit regardless of the bit order (endianness) of the architecture.

 

 x = 5              # Same as 0b0101 a = x[0]           # 1 b = x[1]           # 0 c = x[2]           # 1 d = x[3]           # 0 # Etc.             # 0 

It isn't possible to assign bits using this notation (because a Fixnum is stored as an immediate value rather than an object reference). However, you can always fake it by left-shifting a 1 to the specified bit position and then doing an OR or AND operation.

 

 # We can't do x[3] = 1 # but we can do: x |= (1<<3) # We can't do x[4] = 0 # but we can do: x &= ~(1<<4) 

Finding Cube Roots, Fourth Roots, and So On

Ruby has a built-in square root function (Math.sqrt) because that function is so commonly used. But what if you need higher-level roots? If you remember your math, this is easy.

One way is to use logarithms. Recall that e to the x is the inverse of the natural log of x. When multiplying numbers, that is equivalent to adding their logarithms.

 

 x = 531441 cuberoot = Math.exp(Math.log(x)/3.0)     # 81.0 fourthroot = Math.exp(Math.log(x)/4.0)   # 27.0 

However, it is just as easy and perhaps clearer to use fractions with an exponentiation operator (which can take any integer or floating-point value).

 

 y = 4096 cuberoot = y**(1.0/3.0)      # 16.0 fourthroot = y**(1.0/4.0)    # 8.0 fourthroot = sqrt(sqrt(y))   # 8.0 (same thing) twelfthroot = y**(1.0/12.0)  # 2.0 

Note that in all of these examples, we have used floating-point numbers when dividing (to avoid truncation to an integer).

Rounding Floating-Point Values

If you want to round a floating-point value to an integer, the method round will do the trick.

 

 pi = 3.14159 new_pi = pi.round   # 3 temp = -47.6 temp2 = temp.round  # -48 

Sometimes we want to round not to an integer, but to a specific number of decimal places. In this case, we could use sprintf (which knows how to round) and eval to do this.

 

 pi = 3.1415926535 pi6 = eval(sprintf("%8.6f",pi))  # 3.141593 pi5 = eval(sprintf("%8.5f",pi))  # 3.14159 pi4 = eval(sprintf("%8.4f",pi))  # 3.1416 

Of course, this is somewhat ugly. Let's encapsulate this behavior in a method that we'll add to Float.

 

 class Float   def roundf(places)     temp = self.to_s.length     sprintf("%#{ temp} .#{ places} f",self).to_f   end end 

Occasionally, we follow a different rule in rounding to integers. The tradition of rounding n+0.5 upward results in slight inaccuracies at times; after all, n+0.5 is no closer to n+1 than it is to n. So there is an alternative tradition that rounds to the nearest even number in the case of 0.5 as a fractional part. If we wanted to do this, we might extend the Float class with a method of our own called round2, as shown here.

 

 class Float   def round2     whole = self.floor     fraction = self - whole     if fraction == 0.5       if (whole % 2) == 0         whole       else         whole+1       end     else       self.round     end   end end a = (33.4).round2  # 33 b = (33.5).round2  # 34 c = (33.6).round2  # 34 d = (34.4).round2  # 34 e = (34.5).round2  # 34 f = (34.6).round2  # 35 

Obviously round2 differs from round only when the fractional part is exactly 0.5; note that 0.5 can be represented perfectly in binary, by the way. What is less obvious is that this method will work fine for negative numbers also. (Try it.) Also note that the parentheses used here aren't actually necessary, but rather they are used for readability.

Now, what if we wanted to round to a number of decimal places, but we wanted to use the even rounding method? In this case, we could add a method called roundf2 to Float.

 

 class Float   # round2 definition as before   def roundf2(places)     shift = 10**places     (self * shift).round2 / shift.to_f   end end a = 6.125 b = 6.135 x = a.roundf(2)   # 6.12 y = b.roundf(2)   # 6.12 

The code shown here (roundf and roundf2) has certain limitations, in that a large floating-point number will naturally cause problems when it is multiplied by a large power of ten. For these occurrences, error-checking should be added.

Formatting Numbers for Output

To output numbers in a specific format, you can use the printf method in the Kernel module. It is virtually identical to its C counterpart. For more information, see the documentation for the printf method.

 

 x = 345.6789 i = 123 printf("x = %6.2f\n", x)    # x = 345.68 printf("x = %9.2e\n", x)    # x = 3.457e+02 printf("i = %5d\n", i)      # i =   123 printf("i = %05d\n", i)     # i = 00123 printf("i = %-5d\n", i)     # i = 123 

To store a result in a string rather than printing it immediately, sprintf can be used in much the same way. This method returns a string.

 

 str = sprintf("%5.1f",x)    # "345.7" 

Finally, the String class has a % method that performs this same task. The % method has a format string as a receiver; it takes a single argument (or an array of values) and returns a string.

 

 # Usage is 'format % value' str = "%5.1" % x            # "345.7" str = "%6.2, %05d" % [x,i]  # "345.68, 00123" 

Working with Large Integers

The control of large numbers is possible, and like unto that of small numbers, if we subdivide them.

Sun Tzu

In the event it becomes necessary, Ruby programmers can work with integers of arbitrary size. The transition from a Fixnum to a Bignum is handled automatically and transparently.

 

 num1 = 1000000           # One million (10**6) num2 = num1*num1         # One trillion (10**12) puts num1                # 1000000 puts num1.type           # Fixnum puts num2                # 1000000000000 puts num2.type           # Bignum 

The size of a Fixnum will vary from one architecture to another.

Swapping Two Values

This item isn't strictly concerned with numeric data, but we did want to mention it somewhere. In many languages, swapping or exchanging two values requires a temporary variable; in Ruby (as in Perl and some others), this isn't needed. The following statement

 

 x, y = y, x 

will exchange x and y using multiple assignment. Note, of course, that in the case of numbers, we are exchanging the actual values. In the case of most other objects, we are only swapping what amounts to a pointer or referencethat is, changing which variable refers to which object.

Determining the Architecture's Byte Order

It is an interesting fact of the computing world that we cannot all agree on the order in which binary data ought to be stored. Is the most significant bit stored at the higher-numbered address or the lower? When we shove a message over a wire, do we send the most significant bit first, or the least significant?

Believe it or not, it's not entirely arbitrary. There are good arguments on both sides, which we won't delve into here.

For at least 20 years, the terms "little-endian" and "big-endian" have been applied to the two extreme opposites. These apparently were first used by Danny Cohen; refer to his classic article "On Holy Wars and a Plea for Peace" (IEEE Computer, October 1981). The actual terms are derived from the novel Gulliver's Travels by Jonathan Swift.

Most of the time, we don't care what byte order our architecture uses. But what if we do need to know?

Here's one little method that will determine this for us. It will return a string that is LITTLE, BIG, or OTHER. It depends on the fact that the l directive packs in native mode and the N directive unpacks in network order (or big-endian).

 

 def endianness   num=0x12345678   little = "78563412"   big    = "12345678"   native = [num].pack('l')   netunpack = native.unpack('N')[0]   str = "%8x" % netunpack   case str     when little       "LITTLE"     when big       "BIG"     else       "OTHER"   end end puts endianness   # In this case, prints "LITTLE" 

This technique might come in handy if, for example, you are working with binary data (such as scanned image data) imported from another system.

Calculating the MD5 Hash of a String

The MD5 message-digest algorithm produces a 128-bit fingerprint or message digest of a message of arbitrary length. This is in the form of a hash, so the encryption is one way and doesn't allow for the discovery of the original message from the digest. Ruby has an extension for a class to implement MD5; for those interested in the source code, it's in the ext/md5 directory of the standard Ruby distribution.

There are two class methods, new and md5, to create a new MD5 object. There is really no difference in them.

 

 require 'md5' cryptic = MD5.md5 password = MD5.new 

There are four instance methods: clone, digest, hexdigest, and update. The clone method simply copies the object; update is used to add content to the object as follows:

 

 cryptic.update("Can you keep a secret?") 

You can also create the object and add to the message at the same time:

 

 secret = MD5.new("Sensitive data") 

If a string argument is given, it is added to the object using update. Repeated calls are equivalent to a single call with concatenated arguments.

 

 # These two statements... cryptic.update("Shhh! ") cryptic.update("Be very, very quiet!") # ...are equivalent to this one. cryptic.update("Shhh! Be very, very quiet!"). 

The digest method provides a 16-byte binary string containing the 128-bit digest.

The hexdigest method is what we actually find most useful. It provides the digest as an ASCII string of 32 hex characters representing the 16 bytes. This method is equivalent to the following:

 

 def hexdigest   ret = ''   digest.each_byte { |i| ret << sprintf('%02x', i) }   ret end secret.hexdigest  #  "b30e77a94604b78bd7a7e64ad500f3c2" 

In short, you can get an md5 hash as follows:

 

 require 'md5' m = MD5.new("sensitive data").hexdigest 

Calculating a 32-bit CRC

The Cyclic Redundancy Checksum (CRC) is a well-known way of obtaining a signature for a file or other collection of bytes. The CRC has the property that the chance of data being changed and keeping the same CRC is 1 in 2**N, where N is the number of bits in the result (most often 32 bits).

We refer you to the Zlib library for this. This library, created by Ueno Katsuhiro, isn't part of the standard distribution, but is still well known.

The method crc32 will compute a CRC given a string as a parameter:

 

 crc = Zlib::crc32("hello")  # 907060870 

A previous CRC can be specified as an optional second parameter; the result will be as if the strings were concatenated and a single CRC was computed. This can be used, for example, to compute the checksum of a file so large that we can only read it in chunks.

Numerical Computation of a Definite Integral

I'm very good at integral and differential calculus…

W. S. Gilbert, The Pirates of Penzance, Act I

If you want to estimate the value of a definite integral, there is a time-tested technique for doing so. Essentially we are performing what the calculus student will remember as a Riemann sum.

The integrate method shown here will take beginning and ending values for the dependent variable as well as an increment. The fourth parameter (which isn't really a parameter) is a block. This block should evaluate a function based on the value of the dependent variable passed into that block. (Here we are using variable in the mathematical sense, not in the computing sense.) It isn't necessary to define a function to call in this block, but we do so here for clarity.

 

 def integrate(x0, x1, dx=(x1-x0)/1000.0)   x = x0   sum = 0   loop do     y = yield(x)     sum += dx * y     x += dx     break if x > x1   end   sum end def f(x)   x**2 end z = integrate(0.0,5.0) { |x| f(x) } puts z, "\n"           # 41.7291875 

Note that in the preceding example, we are relying on the fact that a block returns a value that yield can retrieve. We also make certain assumptions here. First, we assume that x0 is less than x1 (otherwise an infinite loop will result); you can easily improve the code in details such as this one. Second, we assume that the function can be evaluated at arbitrary points in the specified domain. If at any time we try to evaluate the function at any other point, chaos will ensue. (Such functions generally aren't integrable anyway, at least over that set of x values. Consider the function f(x)=x/(x-3), when x is 3.)

Drawing on our faded memories of calculus, we might compute the result here to be 41.666 or thereabout (5 cubed divided by 3). Why is the answer not as exact as we might like? It is because of the size of the slice in the Riemann sum; a smaller value for dx will result in greater accuracy (at the expense of an increase in runtime).

Finally, we will point out that a function like this is more useful when we have a variety of functions of arbitrary complexity, not just a simple function such as f(x) = x**2.

Trigonometry in Degrees, Radians, and Grads

When it comes to measuring arc, the mathematical or natural unit is the radian, defined in such a way that an angle of one radian will correspond to an arclength equal to the radius of the circle. A little thought will show that there are 2p radians in a circle.

The degree of arc that we use in everyday life is a holdover from ancient Babylonian base-60 units; this system divides the circle into 360 degrees. The less-familiar grad is a pseudo-metric unit defined in such a way that there are 100 grads in a right angle (or 400 in a circle).

Programming languages often default to the radian when calculating trigonometric functions, and Ruby is no exception. But we show here how to do these calculations in degrees or grads, in the event that any of you are engineers.

Because the number of units in a circle is a simple constant, it follows that there are simple conversion factors between all these units. We will define these here and simply use the constant names in subsequent code. As a matter of convenience, we'll stick them in the Math module.

 

 module Math   RAD2DEG  = 360.0/(2.0*PI)  # Radians to degrees   RAD2GRAD = 400.0/(2.0*PI)  # Radians to grads end 

Now we can define new trig functions if we want. Because we are converting to radians in each case, we will divide by the conversion factor we calculated previously. We could place these in the Math module if we wanted, although we don't show it here.

 

 def sin_d(theta)   Math.sin (theta/Math::RAD2DEG) end def sin_g(theta)   Math.sin (theta/Math::RAD2GRAD) end 

Of course, the corresponding cos and tan functions can be similarly defined.

The atan2 function is a little different. It takes two arguments (the opposite and adjacent legs of a right triangle) and returns an angle. Thus we convert the result, not the argument, handling it this way:

 

 def atan2_d(y,x)   Math.atan2(y,x)/Math::RAD2DEG end def atan2_g(y,x)   Math.atan2(y,x)/Math::RAD2GRAD end 

More Advanced Trig: Arcsin, Arccos, and Hyperbolic Functions

Ruby's Math module doesn't provide arcsin and arccos functions, but you can always define your own. Here we don't provide the theory but only the code.

 

 def arcsin(x)   Math.atan2(x, Math.sqrt(1.0-x*x)) end def arccos(x)   Math.atan2(Math.sqrt(1.0-x*x), x) end 

Note that because we used atan2, we don't have to worry about dividing by zero. This is a compelling reason to use atan2, by the way, along with other issues regarding floating-point error and the speed of floating-point division.

Of course, if you prefer the traditional arctan function that is so familiar to mathematicians, you can define it this way.

 

 def arctan(x)   Math.atan2(x,1.0) end 

All the preceding functions could be modified (as you have already seen) to use degrees or grads rather than radians.

The hyperbolic trig functions aren't defined in Math, but they can be defined as follows. We assume here that you're working with real (not complex) numbers.

 

 def sinh(x)   (Math.exp(x)-Math.exp(-x))/2.0 end def cosh(x)   (Math.exp(x)+Math.exp(-x))/2.0 end def tanh(x)   sinh(x)/cosh(x) end 

The inverses of these functions can also be defined.

 

 def asinh(x)   Math.log(x + Math.sqrt(1.0+x**2)) end def acosh(x)   2.0 * Math.log(Math.sqrt((x+1.0)/2.0)+Math.sqrt((x-1)/2.0)) end def atanh(x)   (Math.log (1.0+x) - Math.log(1.0-x)) / 2.0 end 

Finding Logarithms with Arbitrary Bases

When working with logarithms, we frequently use the natural logarithms (or base e, which is sometimes written ln); we can also use the common or base 10 logarithms. These are defined in Ruby as Math.log and Math.log10, respectively.

In computer science, specifically in such areas as coding and information theory, a base 2 log isn't unusual. For example, this tells the minimum number of bits needed to store a number. We define this function here as log2:

 

 def log2(x)   Math.log(x)/Math.log(2) end 

The inverse is obviously 2**x just as the inverse of log x is Math::E**x or Math.exp(x).

Furthermore, this same technique can be extended to any base. In the unlikely event that you ever need a base 7 logarithm, this will do the trick.

 

 def log7(x)   Math.log(x)/Math.log(7) end 

In practice, the denominator should be calculated once and kept around as a constant.

Comparing Floating-Point Numbers

It is a sad fact of life that computers don't represent floating-point values exactly. The following code fragment, in a perfect world, would print "yes"; on every architecture we have tried, it prints "no" instead.

 

 x = 1000001.0/0.003 y = 0.003*x if y == 1000001.0   puts "yes" else   puts "no" end 

The reason, of course, is that a floating-point number is stored in some finite number of bits; and no finite number of bits is adequate to store a repeating decimal with an infinite number of digits.

Because of this inherent inaccuracy in floating-point comparisons, we might find ourselves in situations (like the one we just saw) in which the values we are comparing are the same for all practical purposes, but the hardware stubbornly thinks they are different.

Here is a simple way to ensure that floating-point comparisons are done with a fudge factor; that is, the comparisons will be done to within any tolerance specified by the programmer.

 

 class Float   EPSILON = 1e-6   # 0.000001   def ==(x)     (self-x).abs < EPSILON   end end x = 1000001.0/0.003 y = 0.003*x if y == 1.0         # Using the new ==   puts "yes"        # Now we output "yes" else   puts "no" end 

We might find that we want different tolerances for different situations. For this case, we define a new method equals? as a member of Float. (We name it this in order to avoid confusion with the standard methods equal? and eql?; the latter in particular shouldn't be overridden.)

 

 class Float   EPSILON = 1e-6   def equals?(x, tolerance=EPSILON)     (self-x).abs < tolerance   end end flag1 = (3.1416).equals? Math::PI          # false flag2 = (3.1416).equals?(Math::PI, 0.001)  # true 

We could also use a different operator entirely to represent approximate equality; the =~ operator might be a good choice.

We'll also mention here that there is a BigFloat class (created by Shigeo Kobayashi) that isn't part of the standard Ruby distribution; this extension allows essentially infinite-precision floating-point math. The library can be found in the Ruby Application Archive.

Finding the Mean, Median, and Mode of a Data Set

Given an array x, let's find the mean of all the values in that array. Actually, there are three common kinds of mean. The ordinary or arithmetic mean is what we call the average in everyday life. The harmonic mean is the number of terms divided by the sum of all their reciprocals. And finally, the geometric mean is the nth root of the product of the n values. Each of these is shown as follows:

 

 def mean(x)   sum=0   x.each { |v| sum += v}   sum/x.size end def hmean(x)   sum=0   x.each { |v| sum += (1.0/v)}   x.size/sum end def gmean(x)   prod=1.0   x.each { |v| prod *= v}   prod**(1.0/x.size) end data = [1.1, 2.3, 3.3, 1.2, 4.5, 2.1, 6.6] am = mean(data)   # 3.014285714 hm = hmean(data)  # 2.101997946 gm = gmean(data)  # 2.508411474 

The median value of a data set is the value that occurs approximately in the middle of the set. For this value, half the numbers in the set should be less and half should be greater. Obviously, this statistic will be more appropriate and meaningful for some data sets than others.

 

 def median(x)   sorted = x.sort   mid = x.size/2   sorted[mid] end data = [7,7,7,4,4,5,4,5,7,2,2,3,3,7,3,4] puts median(data)          # 4 

The mode of a data set is the value that occurs most frequently. If there is only one such value, the set is unimodal; otherwise it is multimodal. A multimodal data set is a more complex case that we don't consider here. If interested, you can extend and improve the code shown here.

 

 def mode(x)   sorted = x.sort   a = Array.new   b = Array.new   sorted.each do |x|     if a.index(x) == nil       a << x               # Add to list of values       b << 1               # Add to list of frequencies     else       b[a.index(x)] += 1   # Increment existing counter     end   end   maxval = b.max           # Find highest count   where  = b.index(maxval) # Find index of highest count   a[where]                 # Find corresponding data value end data = [7,7,7,4,4,5,4,5,7,2,2,3,3,7,3,4] puts mode(data)            # 7 

Variance and Standard Deviation

The variance of a set of data is a measure of how spread out the values are. (Here we don't distinguish between biased and unbiased estimates.) The standard deviation, usually represented by a sigma (e) is simply the square root of the variance.

 

 data = [2, 3, 2, 2, 3, 4, 5, 5, 4, 3, 4, 1, 2] def variance(x)   m = mean(x)   sum = 0.0   x.each { |v| sum += (v-m)**2 }   sum/x.size end def sigma(x)   Math.sqrt(variance(x)) end puts variance(data)   # 1.461538462 puts sigma(data)      # 1.20894105 

Note that the preceding variance function makes use of the mean function defined earlier.

Finding a Correlation Coefficient

The correlation coefficient is one of the simplest and most universal of statistical measures. It is a measure of the "linearity" of a set of x-y pairs, ranging from -1.0 (complete negative correlation) to +1.0 (complete positive correlation).

We compute this using the mean and sigma (standard deviation) functions that we defined previously. For an explanation of this tool, consult any statistics text.

The first version we show assumes two arrays of numbers (of the same size).

 

 def correlate(x,y)   sum = 0.0   x.each_index do |i|     sum += x[i]*y[i]   end   xymean = sum/x.size.to_f   xmean  = mean(x)   ymean  = mean(y)   sx = sigma(x)   sy = sigma(y)   (xymean-(xmean*ymean))/(sx*sy) end a = [3, 6, 9, 12, 15, 18, 21] b = [1.1, 2.1, 3.4, 4.8, 5.6] c = [1.9, 1.0, 3.9, 3.1, 6.9] c1 = correlate(a,a)          # 1.0 c2 = correlate(a,a.reverse)  # -1.0 c3 = correlate(b,c)          # 0.8221970228 

The next version is similar, but it operates on a single array, each element of which is an array containing an x-y pair.

 

 def correlate2(v)   sum = 0.0   v.each do |a|     sum += a[0]*a[1]   end   xymean = sum/v.size.to_f   x = v.collect { |a| a[0]}   y = v.collect { |a| a[1]}   xmean  = mean(x)   ymean  = mean(y)   sx = sigma(x)   sy = sigma(y)   (xymean-(xmean*ymean))/(sx*sy) end d = [[1,6.1], [2.1,3.1], [3.9,5.0], [4.8,6.2]] c4 = correlate2(d)           # 0.2277822492 

Finally we show a version that assumes the x-y pairs are stored in a hash. It simply builds on the previous example.

 

 def correlate_h(h)   correlate2(h.to_a) end e = {  1 => 6.1, 2.1 => 3.1, 3.9 => 5.0, 4.8 => 6.2} c5 = correlate_h(e)          # 0.2277822492 

Performing Base Conversions

Obviously any integer can be represented in any base because they are all stored internally in binary. Further, we know that Ruby can deal with integer constants in any of the four commonly-used bases. This means that if we are concerned about base conversions, we must be concerned with strings in some fashion.

If you are concerned with converting a string to an integer, that is covered in "Converting Strings to Numbers (Decimal and Otherwise)."

If you are concerned with converting numbers to strings, that is another matter. The best way to do it is with the % method of the String class. This method formats its argument according to the printf directive found in the string.

 

 hex = "%x" % 1234      # "4d2" oct = "%o" % 1234      # "2322" bin = "%b" % 1234      # "10011010010" 

Converting from one nondecimal base to another can be done with a combination of these techniques.

 

 oct = "2322" hex = "%x" % oct.oct   # "4d2" 

Converting to and from oddball bases such as 5 or 11 is unsupported by Ruby. This is rare enough that we will leave it as an exercise for you.

Generating Random Numbers

If a pseudorandom number is good enough for you, you're in luck. This is what most language implementations supply you with, and Ruby is no exception.

The Kernel method rand will return a pseudorandom floating-point number x such that x>=0.0 and x<1.0.

 

 a = rand      # 0.6279091137 

If it is called with an integer parameter max, it will return an integer in the range 0...max (noninclusive of the upper bound).

 

 n = rand(10)  # 7 

If we want to seed the random number generator, we can do so with the Kernel method srand, which takes a single numeric parameter. If we pass no value to it, it will construct its own using (among other things) the time of day. If we pass a number to it, it will use that number as the seed. This can be useful in testing, when we want a repeatable sequence of pseudorandom numbers from one script invocation to the next.

 

 srand(5) i, j, k = rand(100), rand(100), rand(100) # 26, 45, 56 srand(5) l, m, n = rand(100), rand(100), rand(100) # 26, 45, 56 

Caching Functions for Speed

Suppose that you have a computationally expensive mathematical function that will be called repeatedly in the course of execution. If speed is critical and you can afford to sacrifice a little memory and accuracy, it might be effective to store values in a table and look them up.

In this example, we define an arbitrary function called zeta, which we want to call over a domain of 0.0 to 90.0. The function zeta is defined to be 2 sin x cos x (in degrees). Let's assume that our parameters will be no more accurate than a tenth of a degree. This means that if we want to store these values, we will need a table of about 900 elements.

Let's look at a code fragment.

 

 def zeta(x)   r2d  = 360.0/(2.0*Math::PI)  # Radians to degrees   2.0*(Math.sin (x/r2d))*(Math.cos (x/r2d)) end $fast_zeta = []  (0..900).each { |x| $fast_zeta[x]=zeta(x/10.0)} def fast_zeta(x)   $fast_zeta[(x*10).round] end y1 = zeta(37.5)                   # Slow y2 = fast_zeta(37.5)              # Somewhat faster y3 = $fast_zeta[(37.5*10).round]  # Still faster 

We define an array called $fast_zeta, using a global variable; we then populate it with all the values from zeta(0.0) to zeta(90.0). We define a function called fast_zeta, which will take a parameter, convert it to an index, and find the appropriate entry in the array. As an alternative, we also access the array directly (in the computation of y3).

In our tests, we put each calculation in a tight loop that ran for millions of iterations. We found that, compared with the calculation of y1, the calculation of y2 was about 66% faster. In addition, the calculation of y3 (which avoided the method call overhead) was about 72.5% faster.

At times, this method won't be practical at all. But we present it to you as a simple demonstration of what can be done to increase speed without dropping into C code.

Matrix Manipulation

There is a standard library matrix.rb for this purpose. It is fairly full-featured, with class methods to create matrices in various forms (including identity and zero matrices) and an accessor method to get at the elements in standard x[i,j] form. There are methods to find a determinant, to transpose a matrix, to multiply by another matrix or by a scalar, and so on.

This is a standard library. It is too elaborate to document here in detail.

Complex Numbers

The standard library complex.rb provides most of the functionality anyone would need for working with numbers in the complex plane. Be warned that some of the methods are named with exclamation points when there isn't necessarily a compelling reason to do so.

This is a well-known standard library. We won't document it here because it is too complex (no pun intended).

Formatting Numbers with Commas

There might be better ways to do it, but this one works. We reverse the string to make it easier to do global substitution, and then reverse it again at the end.

 

 def commas(x)   str = x.to_s.reverse   str.gsub!("([0-9]{ 3} )","\\1,")   str.gsub(",$","").reverse end puts commas(123)        # "123" puts commas(1234)       # "1,234" puts commas(12345)      # "12,435" puts commas(123456)     # "123,456" puts commas(1234567)    # "1,234,567" 


   

 

 



The Ruby Way
The Ruby Way, Second Edition: Solutions and Techniques in Ruby Programming (2nd Edition)
ISBN: 0672328844
EAN: 2147483647
Year: 2000
Pages: 119
Authors: Hal Fulton

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net