* Supports base 2 through 36; digit values 10-36 are represented
* as lowercase letters a-z. Input is case-insensitive.
*
- * @param $input String: of digits
- * @param $sourceBase Integer: 2-36
- * @param $destBase Integer: 2-36
- * @param $pad Integer: 1 or greater
- * @param $lowercase Boolean
- * @return String or false on invalid input
- */
-function wfBaseConvert( $input, $sourceBase, $destBase, $pad = 1, $lowercase = true ) {
- $input = strval( $input );
- if( $sourceBase < 2 ||
+ * @param string $input Input number
+ * @param int $sourceBase Base of the input number
+ * @param int $destBase Desired base of the output
+ * @param int $pad Minimum number of digits in the output (pad with zeroes)
+ * @param bool $lowercase Whether to output in lowercase or uppercase
+ * @param string $engine Either "gmp", "bcmath", or "php"
+ * @return string|bool The output number as a string, or false on error
+ */
+function wfBaseConvert( $input, $sourceBase, $destBase, $pad = 1, $lowercase = true, $engine = 'auto' ) {
+ if(
+ $sourceBase < 2 ||
$sourceBase > 36 ||
$destBase < 2 ||
$destBase > 36 ||
- $pad < 1 ||
- $sourceBase != intval( $sourceBase ) ||
- $destBase != intval( $destBase ) ||
- $pad != intval( $pad ) ||
- !is_string( $input ) ||
- $input == '' ) {
+ $sourceBase != (int) $sourceBase ||
+ $destBase != (int) $destBase ||
+ $pad != (int) $pad ||
+ !preg_match( "/^[" . substr( '0123456789abcdefghijklmnopqrstuvwxyz', 0, $sourceBase ) . "]+$/i", $input )
+ ) {
return false;
}
- $digitChars = '0123456789abcdefghijklmnopqrstuvwxyz';
- $inDigits = array();
- $outChars = '';
- // Decode and validate input string
- $input = strtolower( $input );
- for( $i = 0; $i < strlen( $input ); $i++ ) {
- $n = strpos( $digitChars, $input[$i] );
- if( $n === false || $n > $sourceBase ) {
- return false;
+ static $baseChars = array (
+ 10 => 'a', 11 => 'b', 12 => 'c', 13 => 'd', 14 => 'e', 15 => 'f',
+ 16 => 'g', 17 => 'h', 18 => 'i', 19 => 'j', 20 => 'k', 21 => 'l',
+ 22 => 'm', 23 => 'n', 24 => 'o', 25 => 'p', 26 => 'q', 27 => 'r',
+ 28 => 's', 29 => 't', 30 => 'u', 31 => 'v', 32 => 'w', 33 => 'x',
+ 34 => 'y', 35 => 'z',
+
+ '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5,
+ '6' => 6, '7' => 7, '8' => 8, '9' => 9, 'a' => 10, 'b' => 11,
+ 'c' => 12, 'd' => 13, 'e' => 14, 'f' => 15, 'g' => 16, 'h' => 17,
+ 'i' => 18, 'j' => 19, 'k' => 20, 'l' => 21, 'm' => 22, 'n' => 23,
+ 'o' => 24, 'p' => 25, 'q' => 26, 'r' => 27, 's' => 28, 't' => 29,
+ 'u' => 30, 'v' => 31, 'w' => 32, 'x' => 33, 'y' => 34, 'z' => 35
+ );
+
+ if( extension_loaded( 'gmp' ) && ( $engine == 'auto' || $engine == 'gmp' ) ) {
+ $result = gmp_strval( gmp_init( $input, $sourceBase ), $destBase );
+ } elseif( extension_loaded( 'bcmath' ) && ( $engine == 'auto' || $engine == 'bcmath' ) ) {
+ $decimal = '0';
+ foreach( str_split( strtolower( $input ) ) as $char ) {
+ $decimal = bcmul( $decimal, $sourceBase );
+ $decimal = bcadd( $decimal, $baseChars[$char] );
}
- $inDigits[] = $n;
- }
- // Iterate over the input, modulo-ing out an output digit
- // at a time until input is gone.
- while( count( $inDigits ) ) {
- $work = 0;
- $workDigits = array();
+ for( $result = ''; bccomp( $decimal, 0 ); $decimal = bcdiv( $decimal, $destBase, 0 ) ) {
+ $result .= $baseChars[bcmod( $decimal, $destBase )];
+ }
- // Long division...
- foreach( $inDigits as $digit ) {
- $work *= $sourceBase;
- $work += $digit;
+ $result = strrev( $result );
+ } else {
+ $inDigits = array();
+ foreach( str_split( strtolower( $input ) ) as $char ) {
+ $inDigits[] = $baseChars[$char];
+ }
- if( $work < $destBase ) {
- // Gonna need to pull another digit.
- if( count( $workDigits ) ) {
- // Avoid zero-padding; this lets us find
- // the end of the input very easily when
- // length drops to zero.
- $workDigits[] = 0;
- }
- } else {
- // Finally! Actual division!
- $workDigits[] = intval( $work / $destBase );
+ // Iterate over the input, modulo-ing out an output digit
+ // at a time until input is gone.
+ $result = '';
+ while( $inDigits ) {
+ $work = 0;
+ $workDigits = array();
- // Isn't it annoying that most programming languages
- // don't have a single divide-and-remainder operator,
- // even though the CPU implements it that way?
- $work = $work % $destBase;
+ // Long division...
+ foreach( $inDigits as $digit ) {
+ $work *= $sourceBase;
+ $work += $digit;
+
+ if( $workDigits || $work >= $destBase ) {
+ $workDigits[] = (int) ( $work / $destBase );
+ }
+ $work %= $destBase;
}
- }
- // All that division leaves us with a remainder,
- // which is conveniently our next output digit.
- $outChars .= $digitChars[$work];
+ // All that division leaves us with a remainder,
+ // which is conveniently our next output digit.
+ $result .= $baseChars[$work];
- // And we continue!
- $inDigits = $workDigits;
- }
+ // And we continue!
+ $inDigits = $workDigits;
+ }
- while( strlen( $outChars ) < $pad ) {
- $outChars .= '0';
+ $result = strrev( $result );
}
- if ( !$lowercase ) {
- $outChars = strtoupper( $outChars );
+ if( !$lowercase ) {
+ $result = strtoupper( $result );
}
- return strrev( $outChars );
+ return str_pad( $result, $pad, '0', STR_PAD_LEFT );
}
/**
--- /dev/null
+<?php
+
+/**
+ * Tests for wfBaseConvert()
+ */
+class wfBaseConvertTest extends MediaWikiTestCase {
+ public static function provideSingleDigitConversions() {
+ return array(
+ // 2 3 5 8 10 16 36
+ array( '0', '0', '0', '0', '0', '0', '0' ),
+ array( '1', '1', '1', '1', '1', '1', '1' ),
+ array( '10', '2', '2', '2', '2', '2', '2' ),
+ array( '11', '10', '3', '3', '3', '3', '3' ),
+ array( '100', '11', '4', '4', '4', '4', '4' ),
+ array( '101', '12', '10', '5', '5', '5', '5' ),
+ array( '110', '20', '11', '6', '6', '6', '6' ),
+ array( '111', '21', '12', '7', '7', '7', '7' ),
+ array( '1000', '22', '13', '10', '8', '8', '8' ),
+ array( '1001', '100', '14', '11', '9', '9', '9' ),
+ array( '1010', '101', '20', '12', '10', 'a', 'a' ),
+ array( '1011', '102', '21', '13', '11', 'b', 'b' ),
+ array( '1100', '110', '22', '14', '12', 'c', 'c' ),
+ array( '1101', '111', '23', '15', '13', 'd', 'd' ),
+ array( '1110', '112', '24', '16', '14', 'e', 'e' ),
+ array( '1111', '120', '30', '17', '15', 'f', 'f' ),
+ array( '10000', '121', '31', '20', '16', '10', 'g' ),
+ array( '10001', '122', '32', '21', '17', '11', 'h' ),
+ array( '10010', '200', '33', '22', '18', '12', 'i' ),
+ array( '10011', '201', '34', '23', '19', '13', 'j' ),
+ array( '10100', '202', '40', '24', '20', '14', 'k' ),
+ array( '10101', '210', '41', '25', '21', '15', 'l' ),
+ array( '10110', '211', '42', '26', '22', '16', 'm' ),
+ array( '10111', '212', '43', '27', '23', '17', 'n' ),
+ array( '11000', '220', '44', '30', '24', '18', 'o' ),
+ array( '11001', '221', '100', '31', '25', '19', 'p' ),
+ array( '11010', '222', '101', '32', '26', '1a', 'q' ),
+ array( '11011', '1000', '102', '33', '27', '1b', 'r' ),
+ array( '11100', '1001', '103', '34', '28', '1c', 's' ),
+ array( '11101', '1002', '104', '35', '29', '1d', 't' ),
+ array( '11110', '1010', '110', '36', '30', '1e', 'u' ),
+ array( '11111', '1011', '111', '37', '31', '1f', 'v' ),
+ array( '100000', '1012', '112', '40', '32', '20', 'w' ),
+ array( '100001', '1020', '113', '41', '33', '21', 'x' ),
+ array( '100010', '1021', '114', '42', '34', '22', 'y' ),
+ array( '100011', '1022', '120', '43', '35', '23', 'z' )
+ );
+ }
+
+ /**
+ * @dataProvider provideSingleDigitConversions
+ */
+ public function testDigitToBase2( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) {
+ $this->assertSame( $base2, wfBaseConvert( $base3, '3', '2' ) );
+ $this->assertSame( $base2, wfBaseConvert( $base5, '5', '2' ) );
+ $this->assertSame( $base2, wfBaseConvert( $base8, '8', '2' ) );
+ $this->assertSame( $base2, wfBaseConvert( $base10, '10', '2' ) );
+ $this->assertSame( $base2, wfBaseConvert( $base16, '16', '2' ) );
+ $this->assertSame( $base2, wfBaseConvert( $base36, '36', '2' ) );
+ }
+
+ /**
+ * @dataProvider provideSingleDigitConversions
+ */
+ public function testDigitToBase3( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) {
+ $this->assertSame( $base3, wfBaseConvert( $base2, '2', '3' ) );
+ $this->assertSame( $base3, wfBaseConvert( $base5, '5', '3' ) );
+ $this->assertSame( $base3, wfBaseConvert( $base8, '8', '3' ) );
+ $this->assertSame( $base3, wfBaseConvert( $base10, '10', '3' ) );
+ $this->assertSame( $base3, wfBaseConvert( $base16, '16', '3' ) );
+ $this->assertSame( $base3, wfBaseConvert( $base36, '36', '3' ) );
+ }
+
+ /**
+ * @dataProvider provideSingleDigitConversions
+ */
+ public function testDigitToBase5( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) {
+ $this->assertSame( $base5, wfBaseConvert( $base2, '2', '5' ) );
+ $this->assertSame( $base5, wfBaseConvert( $base3, '3', '5' ) );
+ $this->assertSame( $base5, wfBaseConvert( $base8, '8', '5' ) );
+ $this->assertSame( $base5, wfBaseConvert( $base10, '10', '5' ) );
+ $this->assertSame( $base5, wfBaseConvert( $base16, '16', '5' ) );
+ $this->assertSame( $base5, wfBaseConvert( $base36, '36', '5' ) );
+ }
+
+ /**
+ * @dataProvider provideSingleDigitConversions
+ */
+ public function testDigitToBase8( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) {
+ $this->assertSame( $base8, wfBaseConvert( $base2, '2', '8' ) );
+ $this->assertSame( $base8, wfBaseConvert( $base3, '3', '8' ) );
+ $this->assertSame( $base8, wfBaseConvert( $base5, '5', '8' ) );
+ $this->assertSame( $base8, wfBaseConvert( $base10, '10', '8' ) );
+ $this->assertSame( $base8, wfBaseConvert( $base16, '16', '8' ) );
+ $this->assertSame( $base8, wfBaseConvert( $base36, '36', '8' ) );
+ }
+
+ /**
+ * @dataProvider provideSingleDigitConversions
+ */
+ public function testDigitToBase10( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) {
+ $this->assertSame( $base10, wfBaseConvert( $base2, '2', '10' ) );
+ $this->assertSame( $base10, wfBaseConvert( $base3, '3', '10' ) );
+ $this->assertSame( $base10, wfBaseConvert( $base5, '5', '10' ) );
+ $this->assertSame( $base10, wfBaseConvert( $base8, '8', '10' ) );
+ $this->assertSame( $base10, wfBaseConvert( $base16, '16', '10' ) );
+ $this->assertSame( $base10, wfBaseConvert( $base36, '36', '10' ) );
+ }
+
+ /**
+ * @dataProvider provideSingleDigitConversions
+ */
+ public function testDigitToBase16( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) {
+ $this->assertSame( $base16, wfBaseConvert( $base2, '2', '16' ) );
+ $this->assertSame( $base16, wfBaseConvert( $base3, '3', '16' ) );
+ $this->assertSame( $base16, wfBaseConvert( $base5, '5', '16' ) );
+ $this->assertSame( $base16, wfBaseConvert( $base8, '8', '16' ) );
+ $this->assertSame( $base16, wfBaseConvert( $base10, '10', '16' ) );
+ $this->assertSame( $base16, wfBaseConvert( $base36, '36', '16' ) );
+ }
+
+ /**
+ * @dataProvider provideSingleDigitConversions
+ */
+ public function testDigitToBase36( $base2, $base3, $base5, $base8, $base10, $base16, $base36 ) {
+ $this->assertSame( $base36, wfBaseConvert( $base2, '2', '36' ) );
+ $this->assertSame( $base36, wfBaseConvert( $base3, '3', '36' ) );
+ $this->assertSame( $base36, wfBaseConvert( $base5, '5', '36' ) );
+ $this->assertSame( $base36, wfBaseConvert( $base8, '8', '36' ) );
+ $this->assertSame( $base36, wfBaseConvert( $base10, '10', '36' ) );
+ $this->assertSame( $base36, wfBaseConvert( $base16, '16', '36' ) );
+ }
+
+ public function testLargeNumber() {
+ $this->assertSame( '1100110001111010000000101110100', wfBaseConvert( 'sd89ys', 36, 2 ) );
+ $this->assertSame( '11102112120221201101', wfBaseConvert( 'sd89ys', 36, 3 ) );
+ $this->assertSame( '12003102232400', wfBaseConvert( 'sd89ys', 36, 5 ) );
+ $this->assertSame( '14617200564', wfBaseConvert( 'sd89ys', 36, 8 ) );
+ $this->assertSame( '1715274100', wfBaseConvert( 'sd89ys', 36, 10 ) );
+ $this->assertSame( '663d0174', wfBaseConvert( 'sd89ys', 36, 16 ) );
+ }
+
+ public static function provideNumbers() {
+ $x = array();
+ $chars = '0123456789abcdefghijklmnopqrstuvwxyz';
+ for( $i = 0; $i < 50; $i++ ) {
+ $base = mt_rand( 2, 36 );
+ $len = mt_rand( 10, 100 );
+
+ $str = '';
+ for( $j = 0; $j < $len; $j++ ) {
+ $str .= $chars[mt_rand(0, $base - 1)];
+ }
+
+ $x[] = array( $base, $str );
+ }
+ return $x;
+ }
+
+ /**
+ * @dataProvider provideNumbers
+ */
+ public function testIdentity( $base, $number ) {
+ $this->assertSame( $number, wfBaseConvert( $number, $base, $base, strlen( $number ) ) );
+ }
+
+ public function testInvalid() {
+ $this->assertFalse( wfBaseConvert( '101', 1, 15 ) );
+ $this->assertFalse( wfBaseConvert( '101', 15, 1 ) );
+ $this->assertFalse( wfBaseConvert( '101', 37, 15 ) );
+ $this->assertFalse( wfBaseConvert( '101', 15, 37 ) );
+ $this->assertFalse( wfBaseConvert( 'abcde', 10, 11 ) );
+ $this->assertFalse( wfBaseConvert( '12930', 2, 10 ) );
+ $this->assertFalse( wfBaseConvert( '101', 'abc', 15 ) );
+ $this->assertFalse( wfBaseConvert( '101', 15, 'abc' ) );
+ }
+
+ public function testPadding() {
+ $number = "10101010101";
+ $this->assertSame( strlen( $number ) + 5, strlen( wfBaseConvert( $number, 2, 2, strlen( $number ) + 5 ) ) );
+ $this->assertSame( strlen( $number ), strlen( wfBaseConvert( $number, 2, 2, strlen( $number ) - 5 ) ) );
+ }
+}