Last active
October 12, 2019 17:36
-
-
Save nathan-fiscaletti/5c999a60d17ee1bff0a44bb6365c1e6b to your computer and use it in GitHub Desktop.
A class for manipulating and creating custom numeric formatting
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Class used for converting between bases | |
* and custom bases using digit libraries. | |
* | |
* @see https://me.nathanf.tk/2019/05/02/creating-a-custom-numeric-base-in-php/ | |
* | |
* @author Nathan Fiscaletti | |
*/ | |
class Base | |
{ | |
/** | |
* The library to use. | |
* | |
* @var array | |
*/ | |
private $library = []; | |
/** | |
* The base to convert to. | |
* | |
* @var int | |
*/ | |
private $base; | |
/** | |
* Construct the new BaseConverter. | |
* | |
* If base is 36 or below, the library | |
* for this base will be automatically | |
* loaded using [0-9][A-Z]. Otherwise, | |
* you need to load your own library | |
* using the setLibrary and putLibrary | |
* member functions. | |
* | |
* @param int $base The base to convert to. | |
*/ | |
public function __construct(int $base) | |
{ | |
$this->base = $base; | |
if ($base <= 36) { | |
$this->setLibraryToBase36(); | |
} | |
} | |
/** | |
* Set the library to a specific library. | |
* This will override all existing libraries. | |
* | |
* @param array $library The new library. | |
*/ | |
public function setLibrary(array $library) | |
{ | |
$this->library = [-1 => $library]; | |
} | |
/** | |
* Add a new library. | |
* | |
* @param array $library The new library. | |
* @param int $placement The placement to use for this library. | |
* For Base-10: 0 = 1s place, 1 = 10s place, ... | |
*/ | |
public function putLibrary(array $library, int $placement = -1) | |
{ | |
if (count($library) != $this->base) { | |
throw new \Exception( | |
'BaseConverter: Invalid library size. Not equal to base.' | |
); | |
} | |
$this->library[$placement] = $library; | |
return true; | |
} | |
/** | |
* Set the library to base-36 [0-9][a-z]. | |
*/ | |
private function setLibraryToBase36() | |
{ | |
$this->setLibrary( | |
array_merge( | |
explode(' ', '0 1 2 3 4 5 6 7 8 9'), | |
range('a', 'z') | |
) | |
); | |
} | |
/** | |
* Convert a number to the base that this class uses. | |
* | |
* @param int $input The number to convert. | |
* @param int $maxDigits The maximum number of digits the | |
* resulting number may have. | |
* | |
* @return string The number converted to this base. | |
*/ | |
public function parse(int $input, int $maxDigits = -1) | |
{ | |
if (! array_key_exists(-1, $this->library)) { | |
throw new \Exception('Base: Missing default library.'); | |
} | |
// Compile the indexs to retrieve from the library | |
// based on the information provided. | |
$compiled = $this->makeIndexes($input, $maxDigits); | |
// Retrieve the corresponding entries from | |
// the library to build the resulting string. | |
// | |
// Make sure each words has it's first letter | |
// set to Upper Case so that digits are | |
// distinct from one another. | |
$result = []; | |
foreach ($compiled as $placement => $compiledDigit) { | |
$result[] = ucfirst( | |
( | |
array_key_exists($placement, $this->library) | |
? $this->library[$placement][$compiledDigit] | |
: $this->library[-1 ][$compiledDigit] | |
) | |
); | |
} | |
// Return the result. | |
return implode('', array_reverse($result)); | |
} | |
/** | |
* Converts a value using this base back to base-10. | |
* | |
* @param string $value The value to convert to base-10. | |
* | |
* @return number The resulting base-10 number. | |
*/ | |
public function toBase10(string $value) | |
{ | |
$pieces = preg_split('/(?=[A-Z0-9])/', $value); | |
$pieces = array_slice($pieces, 1, count($pieces) - 1); | |
$pieces = array_reverse($pieces); | |
$compiled = []; | |
foreach ($pieces as $placement => $digit) { | |
$digit = strtolower($digit); | |
$compiled[] = ( | |
array_key_exists($placement, $this->library) | |
? array_search($digit, $this->library[$placement], true) | |
: array_search($digit, $this->library[-1 ], true) | |
); | |
} | |
$result = 0; | |
foreach ($compiled as $placement => $index) | |
{ | |
$result += ($index*pow($this->base, $placement)); | |
} | |
return $result; | |
} | |
/** | |
* Generate the indexes for a converted number based on this base. | |
* | |
* @param int $input The number to convert. | |
* @param int $maxDigits The maximum number of digits the | |
* resulting number may have. | |
* | |
* @return array The resulting indexes as an array in reverse order. | |
*/ | |
private function makeIndexes(int $input, int $maxDigits = -1) | |
{ | |
// Make sure our input isn't larger than the maxium | |
// number allowed based on maxDigits and base. | |
if ($input > pow($this->base, $maxDigits) && $maxDigits != -1) { | |
throw new \Exception( | |
'Base: Input is larger than maximum supported '. | |
'number based on library size and maximum digits.' | |
); | |
} | |
// Generate our compiled list of digits. | |
$compiled = []; | |
do { | |
$d = (int)($input / $this->base); | |
$r = $input % $this->base; | |
$compiled[] = $r; | |
$input = $d; | |
} while ($input >= $this->base); | |
if ($input > 0) { | |
$compiled[] = $input; | |
} | |
return $compiled; | |
} | |
/** | |
* Convert a string conforming to the format of this | |
* base to another base. | |
* | |
* @param string $val The value to convert. | |
* @param self|int $base The base to convert to. | |
* | |
* @return string The value. | |
*/ | |
public function convert(string $val, $base) | |
{ | |
if (! ($base instanceof self)) { | |
if (! is_int($base) || $base < 2 || $base > 36) { | |
throw new \Exception( | |
'Base: Inavlid base to convert to. '. | |
'Must be within range of 2 - 36 (inclusive).' | |
); | |
} | |
$base = new self($base); | |
} | |
return $base->parse( | |
$this->toBase10($val) | |
); | |
} | |
/** | |
* Perform a mathematical expression using numbers of this base. | |
* | |
* Example: | |
* | |
* $result = $base2->math( | |
* '(#a * #b) / #c', | |
* [ | |
* 'a' => $base2_13, // $base2_13 is 1101 | |
* 'b' => $base2_10, // $base2_10 is 1010 | |
* 'c' => $base2_2 // $base2_2 is 10 | |
* ] | |
* ); | |
* // result is '1000001' or, in base-10, 65 | |
* | |
* @param string $expression The expression to run. This should contain variables mapped with '#'. | |
* @param array $map The map of variables to use for the expression. | |
*/ | |
public function math(string $expression, array $map) { | |
$map_b10 = []; | |
foreach ($map as $variable => $value) { | |
$map_b10[$variable] = $this->toBase10($value); | |
} | |
$input = $expression; | |
foreach ($map_b10 as $variable => $value) { | |
$input = str_replace('#'.$variable, $value, $input); | |
} | |
$input = str_replace(',', '.', $input); | |
$input = preg_replace('[^0-9\.\+\-\*\%\/\(\)]', '', $input); | |
return $this->parse(eval('return '.$input.';')); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment