The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# Copyright 2010, 2011, 2012, 2013, 2014 Kevin Ryde

# This file is part of Math-NumSeq.
#
# Math-NumSeq is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 3, or (at your option) any later
# version.
#
# Math-NumSeq is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with Math-NumSeq.  If not, see <http://www.gnu.org/licenses/>.

package Math::NumSeq::Repdigits;
use 5.004;
use strict;

use vars '$VERSION', '@ISA';
$VERSION = 69;
use Math::NumSeq;
@ISA = ('Math::NumSeq');
*_is_infinite = \&Math::NumSeq::_is_infinite;
*_to_bigint = \&Math::NumSeq::_to_bigint;

use Math::NumSeq::NumAronson 8; # new in v.8
*_round_down_pow = \&Math::NumSeq::NumAronson::_round_down_pow;

# uncomment this to run the ### lines
#use Smart::Comments;


# use constant name => Math::NumSeq::__('Repdigits');
use constant description => Math::NumSeq::__('Numbers which are a "repdigit", meaning 0, 1 ... 9, 11, 22, 33, ... 99, 111, 222, 333, ..., 999, etc.  The default is decimal, or select a radix.');
use constant i_start => 0;
use constant characteristic_increasing => 1;
use constant characteristic_integer => 1;
use constant values_min => 0;

use Math::NumSeq::Base::Digits
  'parameter_info_array';   # radix parameter

#------------------------------------------------------------------------------
# cf A002275 - repunits
#    A108850 - repunits count of 1 bits

my @oeis_anum = (
                 # OEIS-Catalogue array begin
                 undef,     # 0
                 undef,     # 1
                 'A000225', # radix=2  # 2^i-1
                 'A048328', # radix=3
                 'A048329', # radix=4
                 undef,     # A048330 starts OFFSET=1 value=0
                 'A048331', # radix=6
                 'A048332', # radix=7
                 undef,     # A048333 starts OFFSET=1 value=0
                 'A048334', # radix=9 
                 'A010785', # radix=10  # starting from OFFSET=0 value=0
                 'A048335', # radix=11
                 'A048336', # radix=12
                 'A048337', # radix=13
                 'A048338', # radix=14
                 'A048339', # radix=15
                 'A048340', # radix=16
                 # OEIS-Catalogue array end
                );
sub oeis_anum {
  my ($self) = @_;
  return $oeis_anum[$self->{'radix'}];
}

#------------------------------------------------------------------------------

sub rewind {
  my ($self) = @_;
  my $radix = $self->{'radix'};
  if ($radix < 2) {
    $radix = $self->{'radix'} = 10;
  }

  $self->{'i'} = $self->i_start;
  $self->{'n'} = -1;
  if ($radix != 2) {
    $self->{'inc'} = 1;
    $self->{'digit'} = -1;
  }
}
sub _UNTESTED__seek_to_i {
  my ($self, $i) = @_;
  $self->{'i'} = $i;
  my $radix = $self->{'radix'};
  if ($radix == 2) {
    if ($i == 0) {
      $self->{'n'} = -1;
    } else {
      $self->{'n'} = $self->ith($i-1);
    }
  } else {
    my $digit = $self->{'digit'} = ($i % $radix) - 1;
    my $exp = int($i/$radix);
    $self->{'inc'} = $self->ith($i-$digit);
    $self->{'n'} = $self->{'inc'} * $digit;
  }
}
sub _UNTESTED__seek_to_value {
  my ($self, $value) = @_;
  $self->seek_to_i($self->value_to_i_ceil($value));
}

sub next {
  my ($self) = @_;

  my $i = $self->{'i'}++;
  my $radix = $self->{'radix'};
  if ($radix == 2) {
    if ($i == 31) {
      $self->{'n'} = _to_bigint($self->{'n'});
    }
    if ($i) {
      $self->{'n'} *= 2;
    }
    return ($i, $self->{'n'} += 1);

  } else {
    # ENHANCE-ME: automatic promote to bigint

    my $n = ($self->{'n'} += $self->{'inc'});
    if (++$self->{'digit'} >= $radix) {
      $self->{'inc'} = $self->{'inc'} * $radix + 1;
      $self->{'digit'} = 1;
      $self->{'n'} = ($n += 1); # not ++$n as that gives warnings on overflow
      ### digit: $self->{'digit'}
      ### inc: $self->{'inc'}
      ### $n
    }
    return ($i, $n);
  }
}

sub ith {
  my ($self, $i) = @_;
  my $radix = $self->{'radix'};

  if (_is_infinite ($i)) {
    return $i;
  }

  if ($radix == 2) {
    my $one = ($i >= 31 ? _to_bigint(1) : 1);
    return ($one << $i) - 1;
  }

  if (($i-=1) < 0) {
    return 0;
  }
  my $digit = ($i % ($radix-1)) + 1;
  $i = int($i/($radix-1)) + 1;
  return ($radix ** $i - 1) / ($radix - 1) * $digit;
}

sub pred {
  my ($self, $value) = @_;

  {
    my $int = int($value);
    if ($value != $int) {
      return 0;
    }
    $value = $int;  # prefer BigInt if input BigFloat
  }

  my $radix = $self->{'radix'};
  if ($radix == 2) {
    return ! (($value+1) & $value);

  }
  if ($radix == 10) {
    my $digit = substr($value,0,1);
    return ($value !~ /[^$digit]/);
  }

  my $digit = $value % $radix;
  while ($value = int($value/$radix)) {
    unless (($value % $radix) == $digit) { # false for inf or nan
      return 0;
    }
  }
  return 1;
}

sub value_to_i_ceil {
  my ($self, $value) = @_;
  ### value_to_i_ceil(): $value

  if (_is_infinite ($value)) {
    return $value;
  }
  if ($value <= 0) {
    return 0;
  }
  my $int = int($value);
  if ($value != $int) {
    $int += 1;
  }
  ### $int

  my $radix = $self->{'radix'};
  my @digits = _digit_split_lowtohigh($int, $radix)
    or return 0;  # if $value==0

  my $high_digit = pop @digits;
  my $i = $high_digit + ($radix-1) * scalar(@digits);
  ### $high_digit
  ### $i

  foreach my $digit (reverse @digits) { # high to low
    if ($digit > $high_digit) {
      return $i + 1;
    }
    if ($digit < $high_digit) {
      last;
    }
  }
  return $i;
}
sub value_to_i_floor {
  my ($self, $value) = @_;

  if ($value < 1) {
    return 0;
  }
  if (_is_infinite ($value)) {
    return $value;
  }
  $value = int($value);

  my $radix = $self->{'radix'};
  my @digits = _digit_split_lowtohigh($value, $radix)
    or return 0;  # if $value==0

  my $high_digit = pop @digits;
  my $i = $high_digit + ($radix-1) * scalar(@digits);

  foreach my $digit (reverse @digits) { # high to low
    if ($digit < $high_digit) {
      return $i - 1;
    }
  }
  return $i;
}

# either floor or 1 too big
sub value_to_i_estimate {
  my ($self, $value) = @_;
  ### value_to_i_estimate() ...

  if ($value < 1) {
    return 0;
  }
  if (_is_infinite ($value)) {
    return $value;
  }
  my $radix = $self->{'radix'};
  my ($power, $exp) = _round_down_pow ($value, $radix);
  return int($value/$power)  # high digit
    + ($radix-1) * $exp;
}

#------------------------------------------------------------------------------

{
  my %binary_to_base4 = ('00' => '0',
                         '01' => '1',
                         '10' => '2',
                         '11' => '3');
  my @radix_to_coderef;
  $radix_to_coderef[2] = sub {
    (my $str = $_[0]->as_bin) =~ s/^0b//;  # strip leading 0b
    return reverse split //, $str;
  };
  $radix_to_coderef[4] = sub {
    (my $str = $_[0]->as_bin) =~ s/^0b//; # strip leading 0b
    if (length($str) & 1) {
      $str = "0$str";
    }
    $str =~ s/(..)/$binary_to_base4{$1}/ge;
    return reverse split //, $str;
  };
  $radix_to_coderef[8] = sub {
    (my $str = $_[0]->as_oct) =~ s/^0//;  # strip leading 0
    return reverse split //, $str;
  };
  $radix_to_coderef[10] = sub {
    return reverse split //, $_[0]->bstr;
  };
  $radix_to_coderef[16] = sub {
    (my $str = $_[0]->as_hex) =~ s/^0x//;  # strip leading 0x
    return reverse map {hex} split //, $str;
  };

  sub _digit_split_lowtohigh {
    my ($n, $radix) = @_;
    ### _digit_split_lowtohigh(): $n

    $n || return; # don't return '0' from BigInt stringize

    if (ref $n
        && $n->isa('Math::BigInt')
        && (my $coderef = $radix_to_coderef[$radix])) {
      return $coderef->($_[0]);
    }

    my @ret;
    do {
      push @ret, $n % $radix;
    } while ($n = int($n/$radix));
    return @ret;   # array[0] low digit
  }
}

1;
__END__

=for stopwords Ryde Math-NumSeq repdigit repdigits

=head1 NAME

Math::NumSeq::Repdigits -- repdigits 11, 22, 33, etc

=head1 SYNOPSIS

 use Math::NumSeq::Repdigits;
 my $seq = Math::NumSeq::Repdigits->new (radix => 10);
 my ($i, $value) = $seq->next;

=head1 DESCRIPTION

The sequence of repdigit numbers,

    0, 1 ... 9, 11, 22, 33, ... 99, 111, 222, 333, ..., 999, etc
    starting i=0

comprising repetitions of a single digit.  The default is decimal or a
C<radix> parameter can be given.

=head1 FUNCTIONS

See L<Math::NumSeq/FUNCTIONS> for behaviour common to all sequence classes.

=over 4

=item C<$seq = Math::NumSeq::Repdigits-E<gt>new ()>

=item C<$seq = Math::NumSeq::Repdigits-E<gt>new (radix =E<gt> $r)>

Create and return a new sequence object.

=back

=head2 Random Access

=over

=item C<$value = $seq-E<gt>ith($i)>

Return the C<$i>'th repdigit.

=item C<$bool = $seq-E<gt>pred($value)>

Return true if C<$value> is a repdigit in the given C<radix>.

=item C<$i = $seq-E<gt>value_to_i_ceil($value)>

=item C<$i = $seq-E<gt>value_to_i_floor($value)>

Return the C<$i> index of C<$value>, rounding up or down if C<$value> is not
a repdigit.

=item C<$i = $seq-E<gt>value_to_i_estimate($value)>

Return an estimate of the i corresponding to C<$value>.

=back

=head1 SEE ALSO

L<Math::NumSeq>,
L<Math::NumSeq::RepdigitAny>,
L<Math::NumSeq::Beastly>

=head1 HOME PAGE

L<http://user42.tuxfamily.org/math-numseq/index.html>

=head1 LICENSE

Copyright 2010, 2011, 2012, 2013, 2014 Kevin Ryde

Math-NumSeq is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation; either version 3, or (at your option) any later
version.

Math-NumSeq is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
more details.

You should have received a copy of the GNU General Public License along with
Math-NumSeq.  If not, see <http://www.gnu.org/licenses/>.

=cut