Skip to content

Commit

Permalink
Merge pull request #1 from thunderer/feature-digits
Browse files Browse the repository at this point in the history
introduced converters, major refactoring towards first release
  • Loading branch information
thunderer committed Apr 27, 2015
2 parents f950a5a + fcc361a commit 05a0167
Show file tree
Hide file tree
Showing 17 changed files with 527 additions and 162 deletions.
83 changes: 76 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,97 @@
[![Code Coverage](https://scrutinizer-ci.com/g/thunderer/Numbase/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/thunderer/Numbase/?branch=master)
[![Code Climate](https://codeclimate.com/github/thunderer/Numbase/badges/gpa.svg)](https://codeclimate.com/github/thunderer/Numbase)

Numbase is a small utility for converting numbers between arbitrary bases. It uses PHP GMP extension to handle big integers.
Easily convert numbers between arbitrary bases and symbol sets.

## Requirements

No required dependencies, only PHP >=5.3
This library requires PHP >=5.3 (namespaces) and GMP extension (for handling large numbers exceeding PHP capabilities).

## Installation

This library is available on Packagist under alias `thunderer/numbase`. Act accordingly.
This library is available on [Packagist](https://packagist.org/packages/thunderer/numbase) as `thunderer/numbase`.

## Usage

**The Simplest Way™**

```php
use Thunder\Numbase\Numbase;

$numbase = Numbase::createDefault();

// decimal 15 to hexadecimal number
assert('F' === $numbase->convert(15, 10, 16));
// 64000 decimal to base 32000
assert('20' === $numbase->convert(64000, 10, 32000));
```

Regular usage (see Internals section for more options):

```php
use Thunder\Numbase\Numbase;
use Thunder\Numbase\Formatter\PermissiveFormatter;
use Thunder\Numbase\Symbols\Base62Symbols;

$numbase = new Numbase(new Base62Symbols(), new PermissiveFormatter());
$base62 = new Base62Symbols();
$numbase = new Numbase(new GmpConverter($base62), new StrictFormatter($base62));

// decimal 15 to hexadecimal number
assert('F' === $numbase->convert(15, 10, 16));
```

**Showcase**

Convert number to and from a different set of symbols:

```php
$base10 = new Base10Symbols();
$upper = new StringSymbols('!@#$%^&*()');

$numbase = new Numbase(new GmpDigits($base10), new StrictFormatter($upper));

assert('#!' === $numbase->convert('20', 10, 10));
assert('-$!' === $numbase->convert('-30', 10, 10));

$numbase = new Numbase(new GmpDigits($upper), new StrictFormatter($base10));

assert('20' === $numbase->convert('#!', 10, 10));
assert('-30' === $numbase->convert('-$!', 10, 10));
```

Get array of digit values (for bases too large for any symbol set):

```php
$numbase = new Numbase(new GmpDigits(new Base62Symbols()), new ArrayFormatter());

assert('10' === $numbase->convert(16, 10, 16));
// convert 10^12 to base 99:
assert(array('10', '61', '53', '3', '51', '60', '10')
=== $numbase->convert('10000000000000', 10, 99));
```

## Internals

Numbase is built upon several concepts:

* **converters** that convert numbers to array of numbers of digits,
* **formatters** that take those arrays and return final numbers,
* **symbols** used in converters to check symbols values and to get digits symbols in formatters.

There are several implementations of each concept bundled with this library, for example:

* converters:
* **GmpConverter**: can convert any integer between any base greater than 2, uses `gmp_*()` functions,
* **GmpStrvalConverter**: uses `gmp_strval()` to convert between bases 2 and 62,
* **BaseConvertConverter**: uses `base_convert()` to convert between bases 2 and 32,
* formatters:
* **ArrayFormatter**: returns raw array of digits numbers,
* **StrictFormatter**: returns number as string, throws exception when digit is not found in symbols set,
* **FallbackFormatter**: returns number as string, but returns string with digit values separated by configured separator when any digit is not found in symbols set,
* symbols:
* **ArraySymbols**: takes associative `array(value => symbol)`,
* **Base62Symbols**: contains alphanumeric set of symbols `0-9A-Za-z up` to base 62,
* **StringSymbols**: takes string and splits it assigning consecutive values to each character.

The named constructor `Numbase::createDefault()` uses `GmpConverter`, `StrictFormatter` and `Base62Symbols` as defaults.

## License

See LICENSE file in the main directory of this library.
43 changes: 43 additions & 0 deletions src/Converter/BaseConvertConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php
namespace Thunder\Numbase\Converter;

use Thunder\Numbase\ConverterInterface;
use Thunder\Numbase\SymbolsInterface;

/**
* @author Tomasz Kowalczyk <[email protected]>
*/
final class BaseConvertConverter implements ConverterInterface
{
private $symbols;

public function __construct(SymbolsInterface $symbols)
{
$this->symbols = $symbols;
}

public function convert($source, $sourceBase, $targetBase)
{
if($sourceBase < 2 || $targetBase < 2 || $sourceBase > 36 || $targetBase > 36)
{
$msg = 'Invalid source or target base, must be an integer between 2 and 36!';
throw new \InvalidArgumentException(sprintf($msg));
}

$source = (string)$source;
if(0 === mb_strlen($source))
{
$msg = 'How about a non-empty string?';
throw new \InvalidArgumentException($msg);
}

$digits = str_split((string)base_convert($source, $sourceBase, $targetBase), 1);

// base_convert() returns lowercase characters so they need to be
// uppercased because lowercased come in latter
$digits = array_map('strtoupper', $digits);
$digits = array_map(array($this->symbols, 'getValue'), $digits);

return array_map('strval', $digits);
}
}
82 changes: 82 additions & 0 deletions src/Converter/GmpConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php
namespace Thunder\Numbase\Converter;

use Thunder\Numbase\ConverterInterface;
use Thunder\Numbase\SymbolsInterface;

/**
* @author Tomasz Kowalczyk <[email protected]>
*/
final class GmpConverter implements ConverterInterface
{
private $symbols;

public function __construct(SymbolsInterface $symbols)
{
$this->symbols = $symbols;
}

public function convert($source, $sourceBase, $targetBase)
{
if($sourceBase < 2 || $targetBase < 2)
{
$msg = 'Invalid source or target base, must be an integer greater than one!';
throw new \InvalidArgumentException(sprintf($msg));
}

$source = (string)$source;
if(0 === mb_strlen($source))
{
$msg = 'How about a non-empty string?';
throw new \InvalidArgumentException($msg);
}

$base10 = $this->convertToBase10($source, $sourceBase);
$digits = $this->computeBaseNDigits($base10, $targetBase);

return $digits;
}

private function convertToBase10($source, $sourceBase)
{
$int = gmp_init(0, 10);
$length = mb_strlen($source) - 1;

for($i = 0; $i <= $length; $i++)
{
$pow = gmp_pow($sourceBase, $length - $i);
$mul = gmp_mul($this->symbols->getValue($source[$i]), $pow);
$int = gmp_add($int, $mul);
}

return $int;
}

private function computeBaseNDigits($number, $targetBase)
{
$digits = array();
$length = $this->computeBaseNLength($number, $targetBase);

for($i = 0; $i < $length; $i++)
{
$pow = gmp_pow($targetBase, $length - $i - 1);
$div = gmp_div($number, $pow, GMP_ROUND_ZERO);
$number = gmp_sub($number, gmp_mul($div, $pow));
$digits[] = $div;
}

return array_map('gmp_strval', $digits);
}

private function computeBaseNLength($number, $targetBase)
{
$digits = 0;

while(gmp_cmp($number, gmp_pow($targetBase, $digits)) != -1)
{
$digits++;
}

return $digits ?: 1;
}
}
39 changes: 39 additions & 0 deletions src/Converter/GmpStrvalConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php
namespace Thunder\Numbase\Converter;

use Thunder\Numbase\ConverterInterface;
use Thunder\Numbase\SymbolsInterface;

/**
* @author Tomasz Kowalczyk <[email protected]>
*/
final class GmpStrvalConverter implements ConverterInterface
{
private $symbols;

public function __construct(SymbolsInterface $symbols)
{
$this->symbols = $symbols;
}

public function convert($source, $sourceBase, $targetBase)
{
if($sourceBase < 2 || $targetBase < 2 || $sourceBase > 62 || $targetBase > 62)
{
$msg = 'Invalid source or target base, must be an integer between 2 and 62!';
throw new \InvalidArgumentException(sprintf($msg));
}

$source = (string)$source;
if(0 === mb_strlen($source))
{
$msg = 'How about a non-empty string?';
throw new \InvalidArgumentException($msg);
}

$digits = str_split((string)gmp_strval(gmp_init($source, $sourceBase), $targetBase), 1);
$digits = array_map(array($this->symbols, 'getValue'), $digits);

return array_map('strval', $digits);
}
}
19 changes: 19 additions & 0 deletions src/ConverterInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php
namespace Thunder\Numbase;

/**
* @author Tomasz Kowalczyk <[email protected]>
*/
interface ConverterInterface
{
/**
* Converts number from source to target base and return digits array
*
* @param string $number
* @param int $sourceBase
* @param int $targetBase
*
* @return array
*/
public function convert($number, $sourceBase, $targetBase);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@
namespace Thunder\Numbase\Formatter;

use Thunder\Numbase\FormatterInterface;
use Thunder\Numbase\SymbolsInterface;

/**
* @author Tomasz Kowalczyk <[email protected]>
*/
final class DigitsFormatter implements FormatterInterface
final class ArrayFormatter implements FormatterInterface
{
public function format(array $digits, SymbolsInterface $symbols)
public function format(array $digits, $signed)
{
if($signed)
{
$digits[0] = '-'.$digits[0];
}

return $digits;
}
}
33 changes: 33 additions & 0 deletions src/Formatter/FallbackFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php
namespace Thunder\Numbase\Formatter;

use Thunder\Numbase\FormatterInterface;
use Thunder\Numbase\SymbolsInterface;

/**
* @author Tomasz Kowalczyk <[email protected]>
*/
final class FallbackFormatter implements FormatterInterface
{
private $symbols;
private $separator;

public function __construct(SymbolsInterface $symbols, $separator)
{
$this->symbols = $symbols;
$this->separator = $separator;
}

public function format(array $digits, $signed)
{
$sign = $signed ? '-' : '';
try
{
return $sign.implode('', array_map(array($this->symbols, 'getSymbol'), $digits));
}
catch(\InvalidArgumentException $e)
{
return $sign.implode($this->separator, $digits);
}
}
}
30 changes: 0 additions & 30 deletions src/Formatter/PermissiveFormatter.php

This file was deleted.

11 changes: 9 additions & 2 deletions src/Formatter/StrictFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@
*/
final class StrictFormatter implements FormatterInterface
{
public function format(array $digits, SymbolsInterface $symbols)
private $symbols;

public function __construct(SymbolsInterface $symbols)
{
$this->symbols = $symbols;
}

public function format(array $digits, $signed)
{
return implode('', array_map(array($symbols, 'getSymbol'), $digits));
return ($signed ? '-' : '').implode('', array_map(array($this->symbols, 'getSymbol'), $digits));
}
}
Loading

0 comments on commit 05a0167

Please sign in to comment.