#!/usr/bin/perl
#-------------------------------------------------------------------------------
# Encode a positive integer using the specified digits and vice-versa
# Philip R Brenan at gmail dot com, Appa Apps Ltd, 2017
#-------------------------------------------------------------------------------
# podDocumentation

package Encode::Positive::Digits;
require v5.16.0;
use warnings FATAL => qw(all);
use strict;
use Carp;
use Math::BigInt;

our $VERSION = '20170811';

#1 Encode and decode

sub encode($$)                                                                  # Returns a string which expresses a positive integer in decimal notation as a string using the specified digits. The specified digits can be any characters chosen from the Unicode character set.
 {my ($number, $digits) = @_;                                                   # Decimal integer, encoding digits

  $number =~ m/\A\d+\Z/s or confess "$number is not a positive decimal integer";# Check the number to be encoded

  my @b = split //, $digits;                                                    # Check the encoding digits
  my $b = Math::BigInt->new(scalar @b);
  $b > 1 or confess
   "number of encoding digits supplied($b) too few, must be at least 2";

  return $b[0] if $number == 0;                                                 # A simple case

  my $n = Math::BigInt->new($number);                                           # Convert to BigInt
  my $e = '';                                                                   # Encoded version

  for my $position(0..4*length($number))                                        # Encoding in binary would take less than this number of digits
   {my $p = $b ** $position;
    next if $p < $n;
    return $b[1].($b[0] x $position) if $p == $n;
    for my $divide(reverse 0..$position-1)
     {my $P = $b ** $divide;
      my $D = int($n / $P);
      $e .= $b[$D];
      $n -= $P*$D;
     }
    return $e;
   }
 }

sub decode($$)                                                                  # Return the integer expressed in decimal notation corresponding to the value of the specified string considered as a number over the specified digits
 {my ($number, $digits) = @_;                                                   # Number to decode, encoding digits

  my @b = split //, $digits;
  my $b = @b;
  $b > 1 or confess
   "number of decoding digits supplied($b) too few, must be at least 2";

  my @n = split //, $number;
  my $n = @n;

  for(1..$n)                                                                    # Convert each digit to be decoded with its decimal equivalent
   {my $d = $n[$_-1];
    my $i = index($digits, $d);
    $i < 0 and confess "Invalid digit \"$d\" in number $number at position $_";
    $n[$_-1] = $i;
   }

  my $p = Math::BigInt->new(1);
  my $s = Math::BigInt->new(0);
  for(reverse @n)                                                               # Decode each digit
   {$s += $p * $_;
    $p *=  $b;
   }

  "$s"
 }

#-------------------------------------------------------------------------------
# Export
#-------------------------------------------------------------------------------

require Exporter;

use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);

@ISA          = qw(Exporter);
@EXPORT       = qw();
@EXPORT_OK    = qw();
%EXPORT_TAGS  = (all=>[@EXPORT, @EXPORT_OK]);

# podDocumentation
=pod

=encoding utf-8

=head1 Name

Encode::Positive::Digits - Encode a positive integer using the specified digits and vice-versa

=head1 Synopsis

 use Encode::Positive::Digits;

 ok 101 == Encode::Positive::Digits::encode(  "5", "01");
 ok   5 == Encode::Positive::Digits::decode("101", "01");

 ok "hello world" eq Encode::Positive::Digits::encode(4830138323689, " abcdefghlopqrw");
 ok 4830138323689 == Encode::Positive::Digits::decode("hello world", " abcdefghlopqrw");

The numbers to be encoded or decoded can be much greater than 2**64 via support
from L<Math::BigInt>, such numbers should be placed inside strings to avoid
inadvertent truncation.

  my $n = '1'.('0'x999).'1';

  my $d = Encode::Positive::Digits::decode($n, "01");
  my $e = Encode::Positive::Digits::encode($d, "01");

  ok $n == $e

  ok length($d) ==  302;
  ok length($e) == 1001;
  ok length($n) == 1001;

=head1 Description

=head2 Encode and decode

=head3 encode

Returns a string which expresses a positive integer in decimal notation as a string using the specified digits. The specified digits can be any characters chosen from the Unicode character set.

  1  $number  Decimal integer
  2  $digits  Encoding digits

=head3 decode

Return the integer expressed in decimal notation corresponding to the value of the specified string considered as a number over the specified digits

  1  $number  Number to decode
  2  $digits  Encoding digits


=head1 Index


L<decode|/decode>

L<encode|/encode>

=head1 Installation

This module is written in 100% Pure Perl and, thus, it is easy to read, use,
modify and install.

Standard Module::Build process for building and installing modules:

  perl Build.PL
  ./Build
  ./Build test
  ./Build install

=head1 Author

L<philiprbrenan@gmail.com|mailto:philiprbrenan@gmail.com>

L<http://www.appaapps.com|http://www.appaapps.com>

=head1 Copyright

Copyright (c) 2016-2017 Philip R Brenan.

This module is free software. It may be used, redistributed and/or modified
under the same terms as Perl itself.

=cut


# Tests and documentation

sub test
 {my $p = __PACKAGE__;
  return if eval "eof(${p}::DATA)";
  my $s = eval "join('', <${p}::DATA>)";
  $@ and die $@;
  eval $s;
  $@ and die $@;
 }

test unless caller;

1;
# podDocumentation
__DATA__
use Test::More tests=>649;

if (1)
 {for my $i(0..127)
   {my $b = Encode::Positive::Digits::encode($i, "01");
    ok $b eq sprintf("%b", $i);
    ok "$i" eq Encode::Positive::Digits::decode($b, "01");
    my $x = Encode::Positive::Digits::encode($i, "0123456789abcdef");
    ok $x eq sprintf("%x", $i);
    ok "$i" eq Encode::Positive::Digits::decode($x, "0123456789abcdef");
    ok "$i" eq Encode::Positive::Digits::encode($i, "0123456789");
   }
 }

if (1)
 {ok 101 == Encode::Positive::Digits::encode(  "5", "01");
  ok   5 == Encode::Positive::Digits::decode("101", "01");
 }

if (1)
 {ok 4830138323689 == Encode::Positive::Digits::decode("hello world", " abcdefghlopqrw");
  ok "hello world" eq Encode::Positive::Digits::encode(4830138323689, " abcdefghlopqrw");
 }

if (1)
 {my $n = '1'.('0'x999).'1';

  my $d = Encode::Positive::Digits::decode($n, "01");
  my $e = Encode::Positive::Digits::encode($d, "01");

  ok length($d) ==  302;
  ok length($e) == 1001;
  ok length($n) == 1001;

  ok $d == '10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069377';
  ok $n == $e
 }