parse($num); if ($num2 >= $denom) { $num2 = $denom - 1; } $this->base = strlen($base) == 0 ? 10 : $base; if ($this->base != 10) { $num1 = base_convert($num1, $this->base, 10); $num2 = base_convert($num2, $this->base, 10); $denom = base_convert($denom, $this->base, 10); } for ($i = $num1; $i <= $num2; $i++) { $this->fraction[$i] = new FractionObj($i, $denom, $base); } } private function parse($n) { $nrange = array(); if (preg_match("/^([0-9a-zA-Z]+)\s*-\s*([0-9a-zA-Z]+)$/", $n, $matches)) { $nrange[] = $matches[1]; $nrange[] = $matches[2]; } else { $nrange[] = $n; $nrange[] = $n; } return $nrange; } } /* CLASS FractionObj A fraction object is instantiated with three pieces of data: a numerator, a denominator, and a number base. PROPERTIES private: $prime: array of prime numbers. public: $num: numerator. $denom: denominator. $reduced_num: reduced numerator. $reduced_denom: reduced denominator. $base: number base. $fraction: fraction description. $section_list: list of sections in a decimal expansion. Sections are: non-repeating, initial repeating portion, repeating portion complement. $decimal: decimal expansion. $decimal_places: number of decimal places in expansion. $repeating: number of repeating decimals in expansion. For example, if the fraction is 1/6 (base 10): $num: 1 $denom: 6 $reduced_num: 1 $reduced_denom: 6 $base: 10 $fraction: 1 / 6 $decimal_places: 2 $repeating: 1 $decimal: .16 $section_list: ('1', '6', ''); If the fraction is 3/6 (base 10): $num: 3 $denom: 6 $reduced_num: 1 $reduced_denom: 2 $base: 10 $fraction: 3 / 6 (1 / 2) $decimal_places: 1 $repeating: 0 $decimal: .5 $section_list: ('5', '', ''); If the fraction is 1/7 (base 10): $num: 1 $denom: 7 $reduced_num: 1 $reduced_denom: 7 $base: 10 $fraction: 1 / 7 $decimal_places: 6 $repeating: 6 $decimal: .142857 $section_list: ('', '142', '857'); METHODS PUBLIC __construct: Object constructor. Sets the public properties directly, or else calls the methods that sets them. PRIVATE calc_decimal: Calculate the decimal expansion for the specified numerator, denominator, and base. Calculation is performed using long- division, rather than relying on the computer's internal precision. In this way, the calculation can be performed to far more decimal places than otherwise possible. PRIVATE count_non_repeating. Calculate and return the number of decimal places that do not repeat, or -1 if the fraction resolves. The number of non-repeating decimal places is calculated by comparing the denominator's prime factors with those of the number base. PRIVATE function get_gcf: Calculate and return the greatest common factor of two integers. */ class FractionObj { public $num, $denom, $reduced_num, $reduced_denom, $base, $fraction, $section_list, $decimal, $decimal_length, $repeating; private $primes = array( -1, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97 ); public function __construct($num, $denom, $base) { $this->num = $num; $this->denom = $denom; $gcf = $this->get_gcf($num, $denom); // GCF of numerator and denominator, for ensuring a reduced fraction. $this->reduced_num = $num / $gcf; $this->reduced_denom = $denom / $gcf; $this->base = $base; if ($this->reduced_denom < $this->denom) { $this->fraction = "$this->num / $this->denom ($this->reduced_num / $this->reduced_denom)"; } else { $this->fraction = "$this->reduced_num / $this->reduced_denom"; } // calc_decimal sets three additional properties: decimal_length, repeating, and decimal. $this->calc_decimal(); } private function calc_decimal() { // (Due to the complexity of the process, this method is copiously commented; hopefully this will clarify rather than obfuscate.) // ----- BEGIN INITIALIZATION ----- // $section_list is a list of the decimal expansion's sections; to be imploded at the end of the calc_decimal method. // The possible sections are: // non-repeating portion, // initial repeating portion, // repeating portion complement. // Examples: // 1/2 = .5. Non-repeating portion: 5; no repeating portions. // 1/6 = .1666... Non-repeating portion: 1; initial repeating portion: 6; no repeating portion complement. // 1/7 = .142857... No non-repeating potion; initial repeating portion: 142; repeating potion complement: 857. $section_list = array('', '', ''); $section_ndx = 0; // Call the count_non_repeating method to determine the number of non-repeating digits in the decimal expansion. // If the fraction resolves (i.e., has no repeating portion), the count_non_repeating method returns -1. $non_repeating = $this->count_non_repeating(); // If a decimal expansion repeats, non_repeating_tally is used to detect the position at which the repetition begins. // If there is no repetition, non_repeating_tally is set to one less than the denominator, which is a position at or beyond the decimal expansion's maximum possible length. // Examples: 1/3 = .33333... The whole thing repeats, so non_repeating_tally is 0. // 1/4 = .25 The fraction resolves. non_repeating_tally is one 3. // 1/6 = .1666... Only the 6's repeat, so non_repeating_tally is 1. $non_repeating_tally = $non_repeating == -1 ? $this->reduced_denom - 1 : $non_repeating; // The remainder_flag array contains one element for every possible remainder of a division involving the denominator. This means the number of elements is one less than the denominator. $remainder_flag_list = array_fill(1, $this->reduced_denom - 1, false); $repeating_decimal_flag = $non_repeating != -1; $start_repeat = 0; $step_digit = 0; $step_remainder = $this->reduced_num; $decimal_length = 0; $linelength = 54; // ----- END INITIALIZATION ----- // ----- BEGIN LONG DIVISION ----- // Continue the process as long as both of two conditions are true: // a) there is a remainder, // b) the current remainder ($step_remainder) has not yet been encountered. while ($step_remainder != 0 && !$remainder_flag_list[$step_remainder]) { // Mark the current remainder in the remainder flag list. // While a decimal expansion can easily contain multiple instances of a certain digit, each possible remainder can occur only once. // This means that if the process encounters a given remainder for a second time, it's begun to repeat itself; ergo, the process is finished. $remainder_flag_list[$step_remainder] = true; // This is where you add a zero to the number you're dividing and then see how many times the divisor goes into it. // The number of times is called "step_digit" because, of course, this is the digit that results from this step of the process. // The step_digits are concatenated at each step, so that the final string is the decimal expansion. $step_digit = floor($step_remainder * $this->base / $this->reduced_denom); // Check for the end of the non-repeating portion of the decimal. if ($decimal_length == $non_repeating_tally) { $section_ndx++; $start_repeat = $step_remainder; // Store this digit; it'll be used to determine when/if the complement begins. } // Append the current digit to the decimal expansion. Perform base conversion if not in base 10. $section_list[$section_ndx] .= ($this->base != 10 ? base_convert($step_digit, 10, $this->base) : $step_digit); // This is where you find the remainder in the current step of the division. // Example: for 1/4, the first step would entail multiplying the 1 by 10 and determining how many times 4 divides the result. Since 4 goes into 10 twice, // the "step_digit" value is 2. Since 10 - 8 is 2, the "step_remainder" value is 2. $step_remainder = $step_remainder * $this->base - $step_digit * $this->reduced_denom; // Here's where we detect the repeating portion's complement. // If in the course of calculating the decimal expansion one encounters two remainders whose sum is the denominator, // then repeating portion can be split into two halves, and the sum of the two halves will be one less than an integer power of the number base. // For instance, in base 10, 1/7 = .142857... The first remainder is 1 (the numerator), and the third remainder is 6. The two halves, 142 and 857, // add up to 999, which is 10^3 - 1. if ($step_remainder + $start_repeat == $this->reduced_denom) { $section_ndx++; } $decimal_length++; // This just checks to see if the decimal expansion has more digits than we want to display on a single line. If so, an HTML line break is included. // ...and we're not going to do this here. The br, if needed, should be inserted by whatever outputs the thing. /* if (is_int($decimal_length / $linelength)) { $section_list[$section_ndx] .= '
'; } */ } // ----- END LONG DIVISION ----- // If the fraction evaluates to a repeating decimal, calculate the number of digits that repeat; otherwise, set it to 'None'. $repeating = $repeating_decimal_flag ? $decimal_length - $non_repeating_tally : 'None'; // Decorate the sections (optional) // $section_list[0] = '' . $section_list[0] . ''; // $section_list[1] = '' . $section_list[1] . ''; // $section_list[2] = '' . $section_list[2] . ''; // Set properties determined by this method. $this->decimal_length = $decimal_length; $this->repeating = $repeating; $this->section_list = $section_list; $this->decimal = '.' . implode($section_list); } private function count_non_repeating() { $base_factor_tally = 0; $base_tmp = $this->base; $denom_tmp = $this->reduced_denom; $max_non_repeat_tally = 0; $non_repeat_tally = 0; $prime_ndx = 1; $retval = 0; // Loop through the prime number list, starting with 2 and continuing through the largest prime factor of the number base. while ($this->primes[$prime_ndx] <= $base_tmp) { // $non_repeat_tally must be initialized to 0 for each prime number in the cycle, so that the maximum tally can be determined. $non_repeat_tally = 0; $base_factor_tally = 0; // First, tally the occurrences of the present prime number in the base's set of factors. while (is_int($base_tmp / $this->primes[$prime_ndx])) { $base_tmp /= $this->primes[$prime_ndx]; $base_factor_tally++; } // If the present prime is indeed a factor of the base, process this block. if ($base_factor_tally > 0) { // This block will determine the number of non-repeating decimal places generated for the current prime number. The maximum value from this will be returned. $prime_power = pow($this->primes[$prime_ndx], $base_factor_tally); // First, divide out all complete sets of the present prime number from the denominator, and tally the number of divisions. while (is_int($denom_tmp / $prime_power)) { $denom_tmp /= $prime_power; $non_repeat_tally++; } // Second, check for any remaining instances of the prime number. If there are any, increment the tally, and divide out all that remain. if (is_int($denom_tmp / $this->primes[$prime_ndx])) { $non_repeat_tally++; while (is_int($denom_tmp / $this->primes[$prime_ndx])) { $denom_tmp /= $this->primes[$prime_ndx]; } } // Keep the $max_non_repeat_tally up-to-date. $max_non_repeat_tally = max($non_repeat_tally, $max_non_repeat_tally); } $prime_ndx++; } // If $denom_tmp is equal to 1 at this point, the denominator's prime factors are all included in the set of factors for the base, which means the decimal resolves. // If the denominator has no factors in common with the base, then there are no non-repeating digits. $retval = $denom_tmp == 1 ? -1 : $max_non_repeat_tally; return $retval; } private function get_gcf($int1, $int2) { $ints = array($int1, $int2); $finished = false; while (!$finished) { $ints = array(max($ints), min($ints)); if (is_int($ints[0] / $ints[1])) { $finished = true; } else { $ints[0] = $ints[0] - $ints[1]; } } return $ints[1]; } } ?>