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];
}
}
?>