http://www.perlmonks.org?node_id=1197777

Anonymous Monk has asked for the wisdom of the Perl Monks concerning the following question:

Hi,

I am sure this is completely trivial (or at least there are some neat tricks for doing this).

I need to enumerate (starting from an arbitrary point) four characters, each of which can be any of [0-9a-z]. So each character goes 0,1,2,3,4,5,6,7,8,9,a,...x,y,z at which point it goes back to 0, and the character to the immediate left is incremented.

This is not difficult to do the brute force way (with a lot of nested if statements), but I am sure there has to be a better method.

Replies are listed 'Best First'.
Re: incrementing mixed letters and numbers
by poj (Abbot) on Aug 22, 2017 at 06:46 UTC
    #!perl use strict; use Math::Base36 ':all'; my $number = decode_base36('9999'); print lc encode_base36(++$number)." " for 1..100;
    poj
      That certainly wins the "don't reinvent the wheel" award. :)

      I ended up going with something a bit more self-contained (do need POSIX though, just to get started). Removing the validation checks from the snippet below, where $ARGV[0] is the first value, and $ARGV1 is the count, I ended up with:

      #!/usr/bin/perl -w use strict; use POSIX; use feature 'state'; my $b36str = lc $ARGV[0]; my $begnum = POSIX::strtol($b36str,36); for (my $i = $begnum; $i < $begnum+$ARGV[1]; $i++) { $b36str = base36($i); print "$b36str\n"; } sub base36 { my ($tmpval) = @_; state $digits = join '', '0'..'9', 'a'..'z'; my $newstr = ''; while ($tmpval) { $newstr = substr($digits, $tmpval % 36, 1) . $newstr; $tmpval = int $tmpval / 36; } return $newstr || '0'; }
Re: incrementing mixed letters and numbers
by haukex (Archbishop) on Aug 22, 2017 at 07:34 UTC

    One way to do this is with an iterator, like the "odometer" example from Chapter 4 of Dominus's Higher-Order Perl, here's a version of that. The advantage is that it only generates each item of the sequence on demand. Note it doesn't necessarily need to be in a package, I just did that so I could overload the <> operator, it's also possible to just call the code reference returned by new as $odo->(). This doesn't implement the "starting from an arbitrary point" that you mention, because it depends a bit on how that starting point is provided to you - is it a number of iterations, or an existing string like "abcd"?

    Update 2019-10-05: I've now released Algorithm::Odometer::Tiny!

    use warnings; use strict; { package Odometer; use overload '<>' => sub { shift->() }; sub new { my $class = shift; my @w = map { [ 1, ref eq 'ARRAY' ? @$_ : $_ ] } @_; my $done; return bless sub { if ($done) { $done=0; return } my @cur = map {$$_[$$_[0]]} @w; for (my $i=$#w; $i>=0; $i--) { last if ++$w[$i][0]<@{$w[$i]}; $w[$i][0]=1; $done=1 unless $i; } return wantarray ? @cur : join '', @cur; }, $class; } } my $odo = Odometer->new( (['0'..'9','a'..'z']) x 4 ); my $cnt=0; while (<$odo>) { print $_; last if ++$cnt>=100; print ', '; } print "\n"; __END__ 0000, 0001, 0002, 0003, 0004, 0005, 0006, 0007, 0008, 0009, 000a, 000b +, 000c, 000d, 000e, 000f, 000g, 000h, 000i, 000j, 000k, 000l, 000m, 0 +00n, 000o, 000p, 000q, 000r, 000s, 000t, 000u, 000v, 000w, 000x, 000y +, 000z, 0010, 0011, 0012, 0013, 0014, 0015, 0016, 0017, 0018, 0019, 0 +01a, 001b, 001c, 001d, 001e, 001f, 001g, 001h, 001i, 001j, 001k, 001l +, 001m, 001n, 001o, 001p, 001q, 001r, 001s, 001t, 001u, 001v, 001w, 0 +01x, 001y, 001z, 0020, 0021, 0022, 0023, 0024, 0025, 0026, 0027, 0028 +, 0029, 002a, 002b, 002c, 002d, 002e, 002f, 002g, 002h, 002i, 002j, 0 +02k, 002l, 002m, 002n, 002o, 002p, 002q, 002r
Re: incrementing mixed letters and numbers
by shmem (Chancellor) on Aug 22, 2017 at 08:36 UTC

    As poj says. Heh... didn't know that there's a module for that. This is what I have in my toolbox

    my %hash; my @chars = (0..9,'A'..'Z','a'..'z',map{chr$_}32..47,58..64,91..96 +); @hash{@chars} = 0..$#chars; sub encode_base { my ($base,$num) = @_; my ($rem,@ret); while ($num) { push @ret, $chars[($rem = $num % $base)]; $num -= $rem; $num /= $base; } return join '', reverse @ret; } sub decode_base { my ($base, $str) = @_; my $num; $num = $num * $base + $hash{$_} for $str =~ /./g; $num; }

    which is basically the same, but lets you encode/decode with a base up to 91. So,

    my $number = decode_base 36, 1009; say lc encode_base 36, $number++ for 0..36;

    does what you want. Also, these subs can be used to convert from decimal to binary, octal, hex and reverse, using 2, 8 and 16 as base. But I'd use the perl builtins for that ;-)

    perl -le'print map{pack c,($-++?1:13)+ord}split//,ESEL'
Re: incrementing mixed letters and numbers
by thanos1983 (Parson) on Aug 22, 2017 at 08:40 UTC

    Hello Anonymous Monk

    It seems that you have already provided with answer(s) to your problem, but just in case you are wondering there are similar questions with solutions to your problem that you can experiment with (Increment a string with letters and numbers and string increment).

    Hope this helps, BR.

    Seeking for Perl wisdom...on the process of learning...not there...yet!
Re: incrementing mixed letters and numbers
by hdb (Monsignor) on Aug 22, 2017 at 13:31 UTC

    variations_with_repetition in module Algorithm::Combinatorics also does what you want:

    use strict; use warnings; use Algorithm::Combinatorics 'variations_with_repetition'; my @data = (0..9,'a'..'z'); my $iter = variations_with_repetition( \@data, 4 ); while( my $c = $iter->next ) { print "@$c\n"; }

    It does end though when it reaches 'zzzz'.

Re: incrementing mixed letters and numbers
by tybalt89 (Monsignor) on Aug 22, 2017 at 14:28 UTC
    #!/usr/bin/perl -040l # http://perlmonks.org/?node_id=1197777 use strict; use warnings; my @alphabet = (0..9, 'a'..'z'); my %next; @next{@alphabet} = @alphabet[1..$#alphabet]; $_ = '0zyw'; # let's start at some place interesting for my $n ( 1..200 ) { print; s/(.)(z*)$/ $next{$1} . $2 =~ tr!z!0!r /e; # advance }

    Prints:

    0zyw 0zyx 0zyy 0zyz 0zz0 0zz1 0zz2 0zz3 0zz4 0zz5 0zz6 0zz7 0zz8 0zz9 +0zza 0zzb 0zzc 0zzd 0zze 0zzf 0zzg 0zzh 0zzi 0zzj 0zzk 0zzl 0zzm 0zzn + 0zzo 0zzp 0zzq 0zzr 0zzs 0zzt 0zzu 0zzv 0zzw 0zzx 0zzy 0zzz 1000 100 +1 1002 1003 1004 1005 1006 1007 1008 1009 100a 100b 100c 100d 100e 10 +0f 100g 100h 100i 100j 100k 100l 100m 100n 100o 100p 100q 100r 100s 1 +00t 100u 100v 100w 100x 100y 100z 1010 1011 1012 1013 1014 1015 1016 +1017 1018 1019 101a 101b 101c 101d 101e 101f 101g 101h 101i 101j 101k + 101l 101m 101n 101o 101p 101q 101r 101s 101t 101u 101v 101w 101x 101 +y 101z 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 102a 102b 10 +2c 102d 102e 102f 102g 102h 102i 102j 102k 102l 102m 102n 102o 102p 1 +02q 102r 102s 102t 102u 102v 102w 102x 102y 102z 1030 1031 1032 1033 +1034 1035 1036 1037 1038 1039 103a 103b 103c 103d 103e 103f 103g 103h + 103i 103j 103k 103l 103m 103n 103o 103p 103q 103r 103s 103t 103u 103 +v 103w 103x 103y 103z 1040 1041 1042 1043 1044 1045 1046 1047 1048 10 +49 104a 104b 104c 104d 104e 104f
Re: incrementing mixed letters and numbers
by Perlbotics (Archbishop) on Aug 22, 2017 at 17:34 UTC

    Since glob has not been mentioned yet, here's another solution (TIMTOWTDI).

    use strict; use warnings; my $start = 'zzy0'; #-- start including this 'number' my $end = 'zzz5'; #-- end including this 'number' my $set = '{' . join(',', 0..9, 'a'..'z') . '}'; #-- max. range is '0000' .. 'zzzz' for ( glob "$set$set$set$set" ) { print $_,"\n" if /$start/ .. /$end/; }

    Caveat: This approach is not efficient. It can consume lots of memory and time (filtering).

Re: incrementing mixed letters and numbers
by danaj (Friar) on Sep 26, 2017 at 21:11 UTC

    As poj pointed out, it's rather convenient with base conversion modules. I did a review of some generic ones in a blog article. You can also see shmem's interesting module on his followup thread: Math::Base.

    With ntheory and an initial string in $v I'd use something like $v = todigitstring(fromdigits($v,36)+1,36);, possibly adding an extra argument if we want zero padding. Of course we'd only do the fromdigits conversion once if that made sense.

    Looking at performance:

                 Rate base36__A base36__B base____B base____A ntheory_A ntheory_B
    base36__A 0.168/s        --      -42%      -99%      -99%     -100%     -100%
    base36__B 0.290/s       73%        --      -99%      -99%     -100%     -100%
    base____B  22.1/s    13090%     7522%        --      -16%      -91%      -94%
    base____A  26.4/s    15672%     9014%       20%        --      -89%      -93%
    ntheory_A   238/s   141957%    81993%      977%      801%        --      -34%
    ntheory_B   361/s   215202%   124321%     1532%     1265%       52%        --
    
    Math::Base36 is quite slow, though if you're only doing a few calls (vs. 10k) it won't matter. Math::Base is actually quite fast for being all Perl. All of them will benefit if you can convert to decimal, do a bunch of calculations with that native form, then turn it back. Math::Base gives you the interesting alternative of doing a bunch of calculations directly in the base, only converting when needed.I believe the old Math::Fleximal (written as part of a perlmonks discussion) also does this. An aside that Perl6 for bases up to 36 is easy, intuitive, and all built-in.