[m-rev.] for review: fix problems with the char module

Julien Fischer jfischer at opturion.com
Wed Sep 10 17:36:38 AEST 2014


For review by anyone.

----------------------

Fix problems with the char module.

(1) The behaviour of digit_to_int/2 was inconsistent with that of is_digit/2.
The former succeeds for all of 0-9, a-z and A-Z while the latter succeeds only
for 0-9 (i.e. it was possible for digit_to_int/2 to succeed for non-decimal
characters, which is not what was intended in many of it uses).

(2) Predicates involving hexadecimal digits were inconsistently named, they
were "hex digits" in one predicate name, "hex chars" in another.

This change ensures that the following operations are supported for binary,
octal, decimal and hexadecimal digits and that we use a consistent naming
scheme for the predicates that implement them:
     - testing if a character is a digit of the given base
     - conversion to an int
     - conversion from an int

In addition, we also add predicates for supporting these operations for user
defined bases, ranging from 2-36.

library/char.m:
     Add the predicate is_decimal_digit/1, which is a synonym for is_digit/1.

     Add the predicate is_base_digit/2.

     Add the predicates int_to_{binary,octal,decimal,hex}_digit/2 and
     base_int_to_digit/3.

     Add the predicates {binary,octal,decimal,hex}_digit_to_int/2 and
     base_digit_to_int/3.

     Add det function versions of the above.

     Delete the function det_digit_to_int/1 that I added the other day.

     Mark the following as obsolete:
         - is_hex_digit/2
         - int_to_hex_char/2
         - int_to_digit/2
         - det_int_to_digit/1
         - det_int_to_digit/2

     Avoid redundant module qualification in the implementation.

     Mark some C foreign_procs as not modifying the trail.

     Re-order some declarations according to how the coding standard says they
     should be ordered.

library/bitmap.m:
library/integer.m:
library/parsing_utils.m:
library/string.m:
compiler/prog_rep_tables.m:
     Replace calls to obsolete predicates or functions.

NEWS:
     Announce the above changes.

     Add note advising users of digit_to_int/2 to check their code for the
     problem described above.

tests/hard_coded/Mmakefile:
tests/hard_coded/test_char_digits.m:
tests/hard_coded/test_char_digits.exp:
     Add a systematic test for the above predicates.

diff --git a/NEWS b/NEWS
index 3e699d5..8fc219b 100644
--- a/NEWS
+++ b/NEWS
@@ -35,7 +35,34 @@ Changes to the Mercury standard library:

  * Procedures in the store module no longer acquire the global lock.

-* We have added the function det_digit_to_int/1 to the char module.
+* The following predicates and functions have been added to the char module:
+
+    - is_decimal_digit/1
+    - is_base_digit/2
+    - int_to_binary_digit/2
+    - int_to_octal_digit/2
+    - int_to_decimal_digit/2
+    - int_to_hex_digit/2
+    - base_int_to_digit/3, det_base_int_to_digit/2
+    - binary_digit_to_int/2, det_binary_digit_to_int/1
+    - octal_digit_to_int/2, det_octal_digit_to_int/1
+    - decimal_digit_to_int/2, det_decimal_digit_to_int/1
+    - hex_digit_to_int/2, det_hex_digit_to_int/1
+    - base_digit_to_int/3, det_base_digit_to_int/2
+
+  The following predicates in the char module have been deprecated and will
+  either be removed or have their semantics changed in a future release.
+
+    - is_hex_digit/2
+    - int_to_hex_char/2
+    - digit_to_int/2
+    - int_to_digit/2
+    - det_int_to_digit/1, det_int_to_digit/2
+
+  NOTE: existing code that calls the predicate char.digit_to_int/2 and assumes
+  that the call will succeed only for those characters for which
+  char.is_digit/1 is true may be broken.  char.digit_to_int/2 succeeds for
+  the characters 0-9, a-z and A-Z, not just 0-9.

  Changes to the Mercury compiler:

diff --git a/compiler/prog_rep_tables.m b/compiler/prog_rep_tables.m
index 30a7d96..75ce864 100644
--- a/compiler/prog_rep_tables.m
+++ b/compiler/prog_rep_tables.m
@@ -207,7 +207,7 @@ find_number_suffix(String, BeforeNum, Num) :-
      int::in, int::out, list(char)::out) is semidet.

  rev_find_number_suffix([RevHead | RevTail], !Num, !Scale, RevRest) :-
-    ( char.digit_to_int(RevHead, Digit) ->
+    ( char.decimal_digit_to_int(RevHead, Digit) ->
          !:Num = !.Num + (!.Scale * Digit),
          !:Scale = !.Scale * 10,
          rev_find_number_suffix(RevTail, !Num, !Scale, RevRest)
diff --git a/library/bitmap.m b/library/bitmap.m
index 24e1379..3a606f2 100644
--- a/library/bitmap.m
+++ b/library/bitmap.m
@@ -1396,9 +1396,9 @@ to_string_chars(Index, BM, !Chars) :-
          Byte = BM ^ unsafe_byte(Index),
          Mask = n_bit_mask(4),
          ( if
-            char.int_to_hex_char((Byte `unchecked_right_shift` 4) /\ Mask,
+            char.int_to_hex_digit((Byte `unchecked_right_shift` 4) /\ Mask,
                  HighChar),
-            char.int_to_hex_char(Byte /\ Mask, LowChar)
+            char.int_to_hex_digit(Byte /\ Mask, LowChar)
            then
              !:Chars = [HighChar, LowChar | !.Chars],
              to_string_chars(Index - 1, BM, !Chars)
@@ -1433,8 +1433,8 @@ hex_chars_to_bitmap(Str, Index, End, ByteIndex, !BM) :-
          % Each byte of the bitmap should have mapped to a pair of characters.
          fail
        else
-        char.is_hex_digit(Str ^ unsafe_elem(Index), HighNibble),
-        char.is_hex_digit(Str ^ unsafe_elem(Index + 1), LowNibble),
+        char.hex_digit_to_int(Str ^ unsafe_elem(Index), HighNibble),
+        char.hex_digit_to_int(Str ^ unsafe_elem(Index + 1), LowNibble),
          Byte = (HighNibble `unchecked_left_shift` 4) \/ LowNibble,
          !:BM = !.BM ^ unsafe_byte(ByteIndex) := Byte,
          hex_chars_to_bitmap(Str, Index + 2, End, ByteIndex + 1, !BM)
diff --git a/library/char.m b/library/char.m
index 40edc1c..4af004d 100644
--- a/library/char.m
+++ b/library/char.m
@@ -68,8 +68,8 @@
      % Converts an integer to its corresponding character. Aborts
      % if there isn't one.
      %
-:- pred char.det_from_int(int::in, char::out) is det.
  :- func char.det_from_int(int) = char.
+:- pred char.det_from_int(int::in, char::out) is det.

      % Returns the maximum numerical character code.
      %
@@ -141,39 +141,125 @@
      %
  :- pred char.is_binary_digit(char::in) is semidet.

-    % True iff the character is a octal digit (0-7) in the ASCII range.
+    % True iff the character is an octal digit (0-7) in the ASCII range.
      %
  :- pred char.is_octal_digit(char::in) is semidet.

-    % True iff the character is a hexadecimal digit (0-9, a-f, A-F)
-    % in the ASCII range.
+    % True iff the character is a decimal digit (0-9) in the ASCII range.
+    % Synonym for char.is_digit/1.
+    %
+:- pred char.is_decimal_digit(char::in) is semidet.
+
+    % True iff the character is a hexadecimal digit (0-9, a-f, A-F) in the
+    % ASCII range.
      %
  :- pred char.is_hex_digit(char::in) is semidet.
+ 
+    % is_base_digit(Base, Digit):
+    % True iff Digit is a digit in the given Base (0-9, a-z, A-Z).
+    % Throws an exception if Base < 2 or Base > 36.
+    %
+:- pred char.is_base_digit(int::in, char::in) is semidet.
+ 
+    % Convert an integer in the range 0-1 to a binary digit (0 or 1) in the
+    % ASCII range.
+    %
+:- pred char.int_to_binary_digit(int::in, char::out) is semidet.
+
+    % Convert an integer 0-7 to an octal digit (0-7) in the ASCII range.
+    %
+:- pred char.int_to_octal_digit(int::in, char::out) is semidet.
+
+    % Convert an integer 0-9 to a decimal digit (0-9) in the ASCII range.
+    %
+:- pred char.int_to_decimal_digit(int::in, char::out) is semidet.
+
+    % Convert an integer 0-15 to a hexadecimal digit (0-9, A-F) in the ASCII
+    % range.
+    %
+:- pred char.int_to_hex_digit(int::in, char::out) is semidet.
+ 
+    % base_int_to_digit(Base, Int, DigitChar):
+    % True iff `DigitChar' is a decimal digit (0-9) or an uppercase letter
+    % (A-Z) representing the value `Int' (0-35) in the given base.
+    % Throws an exception if `Base' < 2 or `Base' > 36.
+    %
+:- pred char.base_int_to_digit(int::in, int::in, char::out) is semidet.
+
+    % As above, but throw an exception instead of failing.
+    %
+:- func char.det_base_int_to_digit(int, int) = char.

+    % True iff char is a binary digit (0 or 1).
+    % Returns the character's value as a digit (0-1).
+    %
+:- pred char.binary_digit_to_int(char::in, int::out) is semidet.
+
+    % As above, but throws an exception instead of failing.
+    %
+:- func char.det_binary_digit_to_int(char) = int.
+
+    % True iff char is an octal digit (0-7).
+    % Returns the character's value as a digit (0-7).
+    %
+:- pred char.octal_digit_to_int(char::in, int::out) is semidet.
+
+    % As above, but throws an exception instead of failing.
+    %
+:- func char.det_octal_digit_to_int(char) = int.
+
+    % True iff char is a decimal digit (0-9).
+    % Returns the character's value as a digit (0-9).
+    %
+:- pred char.decimal_digit_to_int(char::in, int::out) is semidet.
+
+    % As above, but throws an exception instead of failing.
+    %
+:- func char.det_decimal_digit_to_int(char) = int.
+
+    % True iff char is a hexadecimal digit (0-9, a-z or A-F).
+    % Returns the character's value as a digit (0-15).
+    %
+:- pred char.hex_digit_to_int(char::in, int::out) is semidet.
+
+    % As above, but throws an exception instead of failing.
+    %
+:- func char.det_hex_digit_to_int(char) = int.
+
+    % base_digit_to_int(Base, DigitChar, Int):
+    % True iff `DigitChar' is a decimal digit (0-9) or a letter (a-z, A-Z)
+    % representing the value `Int' (0-35) in the given base.
+    % Throws an exception if `Base' < 2 or `Base' > 36.
+    %
+:- pred char.base_digit_to_int(int::in, char::in, int::out) is semidet.
+
+    % As above, but throws an exception instead of failing.
+    %
+:- func char.det_base_digit_to_int(int, char) = int.
+
+:- pragma obsolete(char.is_hex_digit/2).
  :- pred char.is_hex_digit(char, int).
  :- mode char.is_hex_digit(in, out) is semidet.

-    % Convert an integer 0-15 to a hexadecimal digit (0-9, A-F)
-    % in the ASCII range.
+    % Convert an integer 0-15 to a hexadecimal digit (0-9, A-F) in the ASCII
+    % range.
      %
+:- pragma obsolete(char.int_to_hex_char/2).
  :- pred char.int_to_hex_char(int, char).
  :- mode char.int_to_hex_char(in, out) is semidet.

      % Succeeds if char is a decimal digit (0-9) or letter (a-z or A-Z).
      % Returns the character's value as a digit (0-9 or 10-35).
      %
+:- pragma obsolete(char.digit_to_int/2).
  :- pred char.digit_to_int(char::in, int::out) is semidet.

-    % As above, but calls error/1 instead of failing.
+    % char.int_to_digit(Int, DigitChar):
      %
-:- func char.det_digit_to_int(char) = int.
-
-    % char.int_to_uppercase_digit(Int, DigitChar):
-    %
-    % True iff `Int' is an integer in the range 0-35 and
-    % `DigitChar' is a decimal digit or uppercase letter
-    % whose value as a digit is `Int'.
+    % True iff `Int' is an integer in the range 0-35 and `DigitChar' is a
+    % decimal digit or uppercase letter whose value as a digit is `Int'.
      %
+:- pragma obsolete(char.int_to_digit/2).
  :- pred char.int_to_digit(int, char).
  :- mode char.int_to_digit(in, out) is semidet.
  :- mode char.int_to_digit(out, in) is semidet.
@@ -181,7 +267,9 @@
      % Returns a decimal digit or uppercase letter corresponding to the value.
      % Calls error/1 if the integer is not in the range 0-35.
      %
+:- pragma obsolete(char.det_int_to_digit/1).
  :- func char.det_int_to_digit(int) = char.
+:- pragma obsolete(char.det_int_to_digit/2).
  :- pred char.det_int_to_digit(int::in, char::out) is det.

      % Convert a char to a pretty_printer.doc for formatting.
@@ -227,14 +315,14 @@

  % The information here is duplicated in lookup_token_action in lexer.m.
  % If you update this; you will also need update that.
-char.is_whitespace(' ').
-char.is_whitespace('\t').
-char.is_whitespace('\n').
-char.is_whitespace('\r').
-char.is_whitespace('\f').
-char.is_whitespace('\v').
-
-char.is_alpha(Char) :-
+is_whitespace(' ').
+is_whitespace('\t').
+is_whitespace('\n').
+is_whitespace('\r').
+is_whitespace('\f').
+is_whitespace('\v').
+
+is_alpha(Char) :-
      ( char.is_lower(Char) ->
          true
      ; char.is_upper(Char) ->
@@ -243,7 +331,7 @@ char.is_alpha(Char) :-
          fail
      ).

-char.is_alnum(Char) :-
+is_alnum(Char) :-
      ( char.is_alpha(Char) ->
          true
      ; char.is_digit(Char) ->
@@ -252,7 +340,7 @@ char.is_alnum(Char) :-
          fail
      ).

-char.is_alpha_or_underscore(Char) :-
+is_alpha_or_underscore(Char) :-
      ( Char = '_' ->
          true
      ;
@@ -262,7 +350,7 @@ char.is_alpha_or_underscore(Char) :-
      % We explicitly enumerate here for efficiency.
      % (The information here and in some of the following predicates,
      % e.g. char.lower_upper, is duplicated in lookup_token_action in lexer.m.)
-char.is_alnum_or_underscore(Char) :-
+is_alnum_or_underscore(Char) :-
      ( Char = '0'
      ; Char = '1'
      ; Char = '2'
@@ -334,30 +422,30 @@ char.is_alnum_or_underscore(Char) :-
  %       char.is_alpha_or_underscore(Char)
  %   ).

-char.is_lower(Lower) :-
+is_lower(Lower) :-
      char.lower_upper(Lower, _).

-char.is_upper(Upper) :-
+is_upper(Upper) :-
      ( char.lower_upper(_, Upper) ->
          true
      ;
          fail
      ).

-char.to_lower(C1) = C2 :-
+to_lower(C1) = C2 :-
      char.to_lower(C1, C2).

-char.to_lower(Char, Lower) :-
+to_lower(Char, Lower) :-
      ( char.lower_upper(LowerChar, Char) ->
          Lower = LowerChar
      ;
          Lower = Char
      ).

-char.to_upper(C1) = C2 :-
+to_upper(C1) = C2 :-
      char.to_upper(C1, C2).

-char.to_upper(Char, Upper) :-
+to_upper(Char, Upper) :-
      ( char.lower_upper(Char, UpperChar) ->
          Upper = UpperChar
      ;
@@ -372,71 +460,228 @@ char.to_upper(Char, Upper) :-
  % but these versions are very portable.

  %-----------------------------------------------------------------------------%
+%
+% Digit classification.
+%
+
+is_binary_digit('0').
+is_binary_digit('1').
+
+is_octal_digit('0').
+is_octal_digit('1').
+is_octal_digit('2').
+is_octal_digit('3').
+is_octal_digit('4').
+is_octal_digit('5').
+is_octal_digit('6').
+is_octal_digit('7').
+
+is_decimal_digit('0').
+is_decimal_digit('1').
+is_decimal_digit('2').
+is_decimal_digit('3').
+is_decimal_digit('4').
+is_decimal_digit('5').
+is_decimal_digit('6').
+is_decimal_digit('7').
+is_decimal_digit('8').
+is_decimal_digit('9').
+
+is_digit(D) :- is_decimal_digit(D).
+
+is_hex_digit('0').
+is_hex_digit('1').
+is_hex_digit('2').
+is_hex_digit('3').
+is_hex_digit('4').
+is_hex_digit('5').
+is_hex_digit('6').
+is_hex_digit('7').
+is_hex_digit('8').
+is_hex_digit('9').
+is_hex_digit('a').
+is_hex_digit('b').
+is_hex_digit('c').
+is_hex_digit('d').
+is_hex_digit('e').
+is_hex_digit('f').
+is_hex_digit('A').
+is_hex_digit('B').
+is_hex_digit('C').
+is_hex_digit('D').
+is_hex_digit('E').
+is_hex_digit('F').
+
+is_base_digit(Base, Digit) :-
+    ( ( Base < 2 ; Base > 36) ->
+        error("char.is_base_digit: invalid base")
+    ;
+        base_digit_to_int(Base, Digit, _Int)
+    ).
+
+%-----------------------------------------------------------------------------%
+%
+% Digit to integer conversion.
+%
+
+binary_digit_to_int('0', 0).
+binary_digit_to_int('1', 1).
+
+det_binary_digit_to_int(Digit) = Int :-
+    ( binary_digit_to_int(Digit, Int0) ->
+        Int = Int0
+    ;
+        error("char.binary_digit_to_int failed")
+    ). 
+
+octal_digit_to_int('0', 0).
+octal_digit_to_int('1', 1).
+octal_digit_to_int('2', 2).
+octal_digit_to_int('3', 3).
+octal_digit_to_int('4', 4).
+octal_digit_to_int('5', 5).
+octal_digit_to_int('6', 6).
+octal_digit_to_int('7', 7).
+
+det_octal_digit_to_int(Digit) = Int :-
+    ( octal_digit_to_int(Digit, Int0) ->
+        Int = Int0
+    ;
+        error("char.octal_digit_to_int failed")
+    ).

-char.is_binary_digit('0').
-char.is_binary_digit('1').
-
-char.is_octal_digit('0').
-char.is_octal_digit('1').
-char.is_octal_digit('2').
-char.is_octal_digit('3').
-char.is_octal_digit('4').
-char.is_octal_digit('5').
-char.is_octal_digit('6').
-char.is_octal_digit('7').
-
-char.is_digit('0').
-char.is_digit('1').
-char.is_digit('2').
-char.is_digit('3').
-char.is_digit('4').
-char.is_digit('5').
-char.is_digit('6').
-char.is_digit('7').
-char.is_digit('8').
-char.is_digit('9').
-
-char.is_hex_digit(X) :- char.is_hex_digit(X, _).
-
-char.is_hex_digit('0', 0).
-char.is_hex_digit('1', 1).
-char.is_hex_digit('2', 2).
-char.is_hex_digit('3', 3).
-char.is_hex_digit('4', 4).
-char.is_hex_digit('5', 5).
-char.is_hex_digit('6', 6).
-char.is_hex_digit('7', 7).
-char.is_hex_digit('8', 8).
-char.is_hex_digit('9', 9).
-char.is_hex_digit('a', 10).
-char.is_hex_digit('b', 11).
-char.is_hex_digit('c', 12).
-char.is_hex_digit('d', 13).
-char.is_hex_digit('e', 14).
-char.is_hex_digit('f', 15).
-char.is_hex_digit('A', 10).
-char.is_hex_digit('B', 11).
-char.is_hex_digit('C', 12).
-char.is_hex_digit('D', 13).
-char.is_hex_digit('E', 14).
-char.is_hex_digit('F', 15).
-
-char.int_to_hex_char(0, '0').
-char.int_to_hex_char(1, '1').
-char.int_to_hex_char(2, '2').
-char.int_to_hex_char(3, '3').
-char.int_to_hex_char(4, '4').
-char.int_to_hex_char(5, '5').
-char.int_to_hex_char(6, '6').
-char.int_to_hex_char(7, '7').
-char.int_to_hex_char(8, '8').
-char.int_to_hex_char(9, '9').
-char.int_to_hex_char(10, 'A').
-char.int_to_hex_char(11, 'B').
-char.int_to_hex_char(12, 'C').
-char.int_to_hex_char(13, 'D').
-char.int_to_hex_char(14, 'E').
-char.int_to_hex_char(15, 'F').
+decimal_digit_to_int('0', 0).
+decimal_digit_to_int('1', 1).
+decimal_digit_to_int('2', 2).
+decimal_digit_to_int('3', 3).
+decimal_digit_to_int('4', 4).
+decimal_digit_to_int('5', 5).
+decimal_digit_to_int('6', 6).
+decimal_digit_to_int('7', 7).
+decimal_digit_to_int('8', 8).
+decimal_digit_to_int('9', 9).
+
+det_decimal_digit_to_int(Digit) = Int :-
+    ( decimal_digit_to_int(Digit, Int0) ->
+        Int = Int0
+    ;
+        error("char.decimal_digit_to_int failed")
+    ).
+
+is_hex_digit(Digit, Int) :-
+    hex_digit_to_int(Digit, Int).
+
+hex_digit_to_int('0', 0).
+hex_digit_to_int('1', 1).
+hex_digit_to_int('2', 2).
+hex_digit_to_int('3', 3).
+hex_digit_to_int('4', 4).
+hex_digit_to_int('5', 5).
+hex_digit_to_int('6', 6).
+hex_digit_to_int('7', 7).
+hex_digit_to_int('8', 8).
+hex_digit_to_int('9', 9).
+hex_digit_to_int('a', 10).
+hex_digit_to_int('b', 11).
+hex_digit_to_int('c', 12).
+hex_digit_to_int('d', 13).
+hex_digit_to_int('e', 14).
+hex_digit_to_int('f', 15).
+hex_digit_to_int('A', 10).
+hex_digit_to_int('B', 11).
+hex_digit_to_int('C', 12).
+hex_digit_to_int('D', 13).
+hex_digit_to_int('E', 14).
+hex_digit_to_int('F', 15).
+
+det_hex_digit_to_int(Digit) = Int :-
+    ( hex_digit_to_int(Digit, Int0) ->
+        Int = Int0
+    ;
+        error("char.hex_digit_to_int failed")
+    ).
+
+base_digit_to_int(Base, Digit, Int) :-
+    ( ( Base < 2 ; Base > 36 ) ->
+        error("char.base_digit_to_int: invalid base")
+    ;
+        ( char.lower_upper(Digit, Upper) ->
+            int_to_extended_digit(Int, Upper)
+        ;
+            int_to_extended_digit(Int, Digit)
+        ),
+        Int < Base
+    ).
+
+det_base_digit_to_int(Base, Digit) = Int :-
+    ( base_digit_to_int(Base, Digit, Int0) ->
+        Int = Int0
+    ;
+        error("char.base_digit_to_int failed")
+    ).
+
+%-----------------------------------------------------------------------------%
+%
+% Integer to digit conversion.
+%
+
+int_to_binary_digit(0, '0').
+int_to_binary_digit(1, '1').
+
+int_to_octal_digit(0, '0').
+int_to_octal_digit(1, '1').
+int_to_octal_digit(2, '2').
+int_to_octal_digit(3, '3').
+int_to_octal_digit(4, '4').
+int_to_octal_digit(5, '5').
+int_to_octal_digit(6, '6').
+int_to_octal_digit(7, '7').
+
+int_to_decimal_digit(0, '0').
+int_to_decimal_digit(1, '1').
+int_to_decimal_digit(2, '2').
+int_to_decimal_digit(3, '3').
+int_to_decimal_digit(4, '4').
+int_to_decimal_digit(5, '5').
+int_to_decimal_digit(6, '6').
+int_to_decimal_digit(7, '7').
+int_to_decimal_digit(8, '8').
+int_to_decimal_digit(9, '9').
+
+int_to_hex_digit(0, '0').
+int_to_hex_digit(1, '1').
+int_to_hex_digit(2, '2').
+int_to_hex_digit(3, '3').
+int_to_hex_digit(4, '4').
+int_to_hex_digit(5, '5').
+int_to_hex_digit(6, '6').
+int_to_hex_digit(7, '7').
+int_to_hex_digit(8, '8').
+int_to_hex_digit(9, '9').
+int_to_hex_digit(10, 'A').
+int_to_hex_digit(11, 'B').
+int_to_hex_digit(12, 'C').
+int_to_hex_digit(13, 'D').
+int_to_hex_digit(14, 'E').
+int_to_hex_digit(15, 'F').
+
+int_to_hex_char(Int, Char) :-
+    int_to_hex_digit(Int, Char).
+
+base_int_to_digit(Base, Int, Digit) :-
+    ( ( Base < 2 ; Base > 36) ->
+        error("char.base_int_to_digit: invalid base")
+    ;
+        Int < Base,
+        int_to_extended_digit(Int, Digit)
+    ).
+
+det_base_int_to_digit(Base, Int) = Digit :-
+    ( base_int_to_digit(Base, Int, Digit0) ->
+        Digit = Digit0
+    ;
+        error("char.base_int_to_digit failed")
+    ).

  %-----------------------------------------------------------------------------%

@@ -444,91 +689,91 @@ char.det_int_to_digit(N) = C :-
      char.det_int_to_digit(N, C).

  char.det_int_to_digit(Int, Digit) :-
-    ( char.int_to_digit(Int, Digit1) ->
+    ( int_to_extended_digit(Int, Digit1) ->
          Digit = Digit1
      ;
          error("char.int_to_digit failed")
      ).

-char.int_to_digit(0, '0').
-char.int_to_digit(1, '1').
-char.int_to_digit(2, '2').
-char.int_to_digit(3, '3').
-char.int_to_digit(4, '4').
-char.int_to_digit(5, '5').
-char.int_to_digit(6, '6').
-char.int_to_digit(7, '7').
-char.int_to_digit(8, '8').
-char.int_to_digit(9, '9').
-char.int_to_digit(10, 'A').
-char.int_to_digit(11, 'B').
-char.int_to_digit(12, 'C').
-char.int_to_digit(13, 'D').
-char.int_to_digit(14, 'E').
-char.int_to_digit(15, 'F').
-char.int_to_digit(16, 'G').
-char.int_to_digit(17, 'H').
-char.int_to_digit(18, 'I').
-char.int_to_digit(19, 'J').
-char.int_to_digit(20, 'K').
-char.int_to_digit(21, 'L').
-char.int_to_digit(22, 'M').
-char.int_to_digit(23, 'N').
-char.int_to_digit(24, 'O').
-char.int_to_digit(25, 'P').
-char.int_to_digit(26, 'Q').
-char.int_to_digit(27, 'R').
-char.int_to_digit(28, 'S').
-char.int_to_digit(29, 'T').
-char.int_to_digit(30, 'U').
-char.int_to_digit(31, 'V').
-char.int_to_digit(32, 'W').
-char.int_to_digit(33, 'X').
-char.int_to_digit(34, 'Y').
-char.int_to_digit(35, 'Z').
+:- pred int_to_extended_digit(int, char).
+:- mode int_to_extended_digit(in, out) is semidet.
+:- mode int_to_extended_digit(out, in) is semidet.
+
+int_to_extended_digit(0, '0').
+int_to_extended_digit(1, '1').
+int_to_extended_digit(2, '2').
+int_to_extended_digit(3, '3').
+int_to_extended_digit(4, '4').
+int_to_extended_digit(5, '5').
+int_to_extended_digit(6, '6').
+int_to_extended_digit(7, '7').
+int_to_extended_digit(8, '8').
+int_to_extended_digit(9, '9').
+int_to_extended_digit(10, 'A').
+int_to_extended_digit(11, 'B').
+int_to_extended_digit(12, 'C').
+int_to_extended_digit(13, 'D').
+int_to_extended_digit(14, 'E').
+int_to_extended_digit(15, 'F').
+int_to_extended_digit(16, 'G').
+int_to_extended_digit(17, 'H').
+int_to_extended_digit(18, 'I').
+int_to_extended_digit(19, 'J').
+int_to_extended_digit(20, 'K').
+int_to_extended_digit(21, 'L').
+int_to_extended_digit(22, 'M').
+int_to_extended_digit(23, 'N').
+int_to_extended_digit(24, 'O').
+int_to_extended_digit(25, 'P').
+int_to_extended_digit(26, 'Q').
+int_to_extended_digit(27, 'R').
+int_to_extended_digit(28, 'S').
+int_to_extended_digit(29, 'T').
+int_to_extended_digit(30, 'U').
+int_to_extended_digit(31, 'V').
+int_to_extended_digit(32, 'W').
+int_to_extended_digit(33, 'X').
+int_to_extended_digit(34, 'Y').
+int_to_extended_digit(35, 'Z').
+
+int_to_digit(Int, Digit) :-
+    int_to_extended_digit(Int, Digit).

  char.digit_to_int(Digit, Int) :-
      ( char.lower_upper(Digit, Upper) ->
-        char.int_to_digit(Int, Upper)
-    ;
-        char.int_to_digit(Int, Digit)
-    ).
-
-char.det_digit_to_int(Digit) = Int :-
-    ( digit_to_int(Digit, Int0) ->
-        Int = Int0
+        int_to_extended_digit(Int, Upper)
      ;
-        error("char.digit_to_int failed")
+        int_to_extended_digit(Int, Digit)
      ).

  %-----------------------------------------------------------------------------%

-char.lower_upper('a', 'A').
-char.lower_upper('b', 'B').
-char.lower_upper('c', 'C').
-char.lower_upper('d', 'D').
-char.lower_upper('e', 'E').
-char.lower_upper('f', 'F').
-char.lower_upper('g', 'G').
-char.lower_upper('h', 'H').
-char.lower_upper('i', 'I').
-char.lower_upper('j', 'J').
-char.lower_upper('k', 'K').
-char.lower_upper('l', 'L').
-char.lower_upper('m', 'M').
-char.lower_upper('n', 'N').
-char.lower_upper('o', 'O').
-char.lower_upper('p', 'P').
-char.lower_upper('q', 'Q').
-char.lower_upper('r', 'R').
-char.lower_upper('s', 'S').
-char.lower_upper('t', 'T').
-char.lower_upper('u', 'U').
-char.lower_upper('v', 'V').
-char.lower_upper('w', 'W').
-char.lower_upper('x', 'X').
-char.lower_upper('y', 'Y').
-char.lower_upper('z', 'Z').
+lower_upper('a', 'A').
+lower_upper('b', 'B').
+lower_upper('c', 'C').
+lower_upper('d', 'D').
+lower_upper('e', 'E').
+lower_upper('f', 'F').
+lower_upper('g', 'G').
+lower_upper('h', 'H').
+lower_upper('i', 'I').
+lower_upper('j', 'J').
+lower_upper('k', 'K').
+lower_upper('l', 'L').
+lower_upper('m', 'M').
+lower_upper('n', 'N').
+lower_upper('o', 'O').
+lower_upper('p', 'P').
+lower_upper('q', 'Q').
+lower_upper('r', 'R').
+lower_upper('s', 'S').
+lower_upper('t', 'T').
+lower_upper('u', 'U').
+lower_upper('v', 'V').
+lower_upper('w', 'W').
+lower_upper('x', 'X').
+lower_upper('y', 'Y').
+lower_upper('z', 'Z').

  %-----------------------------------------------------------------------------%

@@ -566,7 +811,7 @@ char.to_int(C) = N :-

  :- pragma foreign_proc("C",
      char.to_int(Character::out, Int::in),
-    [will_not_call_mercury, promise_pure, thread_safe,
+    [will_not_call_mercury, promise_pure, thread_safe, will_not_modify_trail,
          does_not_affect_liveness],
  "
      Character = Int;
@@ -653,7 +898,7 @@ char.max_char_value = N :-

  :- pragma foreign_proc("C",
      char.max_char_value(Max::out),
-    [will_not_call_mercury, promise_pure, thread_safe,
+    [will_not_call_mercury, promise_pure, thread_safe, will_not_modify_trail,
          does_not_affect_liveness],
  "
      Max = 0x10ffff;
diff --git a/library/integer.m b/library/integer.m
index c9a6a0a..a277401 100644
--- a/library/integer.m
+++ b/library/integer.m
@@ -608,7 +608,7 @@ int_to_integer(D) = Int :-
          Int = i(-1, [D])
      ;
          ( int.min_int(D) ->
-            % were we to call int.abs, int overflow might occur.
+            % Were we to call int.abs, int overflow might occur.
              Int = integer(D + 1) - integer.one
          ;
              Int = big_sign(D, pos_int_to_digits(int.abs(D)))
@@ -1263,9 +1263,8 @@ printbase_pos_mul_list([X|Xs], Carry, Y) =

  %-----------------------------------------------------------------------------%

-integer.from_base_string(Base0, String) = Integer :-
+integer.from_base_string(Base, String) = Integer :-
      string.index(String, 0, Char),
-    Base = integer(Base0),
      Len = string.length(String),
      ( Char = ('-') ->
          Len > 1,
@@ -1283,14 +1282,13 @@ integer.from_base_string(Base0, String) = Integer :-
          Integer = N
      ).

-:- pred accumulate_integer(integer::in, char::in, integer::in, integer::out)
+:- pred accumulate_integer(int::in, char::in, integer::in, integer::out)
      is semidet.

  accumulate_integer(Base, Char, !N) :-
-    char.digit_to_int(Char, Digit0),
+    char.base_digit_to_int(Base, Char, Digit0),
      Digit = integer(Digit0),
-    Digit < Base,
-    !:N = (Base * !.N) + Digit.
+    !:N = (integer(Base) * !.N) + Digit.

  integer.det_from_base_string(Base, String) = Integer :-
      ( Integer0 = integer.from_base_string(Base, String) ->
diff --git a/library/parsing_utils.m b/library/parsing_utils.m
index d1135ed..5ead0e2 100644
--- a/library/parsing_utils.m
+++ b/library/parsing_utils.m
@@ -883,8 +883,7 @@ int_literal(Src, Int, !PS) :-

  digits(Base, Src, unit, !PS) :-
      next_char(Src, C, !PS),
-    char.digit_to_int(C, D),
-    D < Base,
+    char.is_base_digit(Base, C),
      digits_2(Base, Src, _, !PS).

  :- pred digits_2(int::in, src::in, unit::out, ps::in, ps::out) is semidet.
@@ -892,11 +891,10 @@ digits(Base, Src, unit, !PS) :-
  digits_2(Base, Src, unit, !PS) :-
      ( if
          next_char(Src, C, !PS),
-        char.digit_to_int(C, D),
-        D < Base
-      then
+        char.is_base_digit(Base, C)
+    then
          digits_2(Base, Src, _, !PS)
-      else
+    else
          true
      ).

diff --git a/library/string.m b/library/string.m
index 03354cc..89442bd 100644
--- a/library/string.m
+++ b/library/string.m
@@ -1193,8 +1193,7 @@ string.base_string_to_int(Base, String, Int) :-
  :- pred accumulate_int(int::in, char::in, int::in, int::out) is semidet.

  accumulate_int(Base, Char, N0, N) :-
-    char.digit_to_int(Char, M),
-    M < Base,
+    char.base_digit_to_int(Base, Char, M),
      N = (Base * N0) + M,
      ( N0 =< N ; Base \= 10 ).           % Fail on overflow for base 10 numbers.

@@ -1202,8 +1201,7 @@ accumulate_int(Base, Char, N0, N) :-
      int::in, int::out) is semidet.

  accumulate_negative_int(Base, Char, N0, N) :-
-    char.digit_to_int(Char, M),
-    M < Base,
+    char.base_digit_to_int(Base, Char, M),
      N = (Base * N0) - M,
      ( N =< N0 ; Base \= 10 ).       % Fail on underflow for base 10 numbers.

@@ -1518,12 +1516,12 @@ string.int_to_base_string_1(N, Base, Str) :-
  string.int_to_base_string_2(NegN, Base, !RevChars) :-
      ( NegN > -Base ->
          N = -NegN,
-        char.det_int_to_digit(N, DigitChar),
+        DigitChar = char.det_base_int_to_digit(Base, N),
          !:RevChars = [DigitChar | !.RevChars]
      ;
          NegN1 = NegN // Base,
          N10 = (NegN1 * Base) - NegN,
-        char.det_int_to_digit(N10, DigitChar),
+        DigitChar = char.det_base_int_to_digit(Base, N10),
          string.int_to_base_string_2(NegN1, Base, !RevChars),
          !:RevChars = [DigitChar | !.RevChars]
      ).
@@ -1609,12 +1607,12 @@ string.int_to_base_string_group_2(NegN, Base, Curr, Period, Sep, Str) :-
      ;
          ( NegN > -Base ->
              N = -NegN,
-            char.det_int_to_digit(N, DigitChar),
+            DigitChar = char.det_base_int_to_digit(Base, N),
              string.char_to_string(DigitChar, Str)
          ;
              NegN1 = NegN // Base,
              N10 = (NegN1 * Base) - NegN,
-            char.det_int_to_digit(N10, DigitChar),
+            DigitChar = char.det_base_int_to_digit(Base, N10),
              string.char_to_string(DigitChar, DigitString),
              string.int_to_base_string_group_2(NegN1, Base, Curr + 1, Period,
                  Sep, Str1),
diff --git a/tests/hard_coded/Mmakefile b/tests/hard_coded/Mmakefile
index 3263b70..687fb47 100644
--- a/tests/hard_coded/Mmakefile
+++ b/tests/hard_coded/Mmakefile
@@ -306,6 +306,7 @@ ORDINARY_PROGS=	\
  	term_io_test \
  	term_to_univ_test \
  	test234_sorted_insert \
+	test_char_digits \
  	test_cord \
  	test_cord2 \
  	test_imported_no_tag \
diff --git a/tests/hard_coded/test_char_digits.exp b/tests/hard_coded/test_char_digits.exp
new file mode 100644
index 0000000..32fa5e2
--- /dev/null
+++ b/tests/hard_coded/test_char_digits.exp
@@ -0,0 +1,2574 @@
+==== Testing binary digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': no  : *  : *
+'3': no  : *  : *
+'4': no  : *  : *
+'5': no  : *  : *
+'6': no  : *  : *
+'7': no  : *  : *
+'8': no  : *  : *
+'9': no  : *  : *
+'a': no  : *  : *
+'b': no  : *  : *
+'c': no  : *  : *
+'d': no  : *  : *
+'e': no  : *  : *
+'f': no  : *  : *
+'g': no  : *  : *
+'h': no  : *  : *
+'i': no  : *  : *
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': no  : *  : *
+'B': no  : *  : *
+'C': no  : *  : *
+'D': no  : *  : *
+'E': no  : *  : *
+'F': no  : *  : *
+'G': no  : *  : *
+'H': no  : *  : *
+'I': no  : *  : *
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing octal digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': no  : *  : *
+'9': no  : *  : *
+'a': no  : *  : *
+'b': no  : *  : *
+'c': no  : *  : *
+'d': no  : *  : *
+'e': no  : *  : *
+'f': no  : *  : *
+'g': no  : *  : *
+'h': no  : *  : *
+'i': no  : *  : *
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': no  : *  : *
+'B': no  : *  : *
+'C': no  : *  : *
+'D': no  : *  : *
+'E': no  : *  : *
+'F': no  : *  : *
+'G': no  : *  : *
+'H': no  : *  : *
+'I': no  : *  : *
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing decimal digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': no  : *  : *
+'b': no  : *  : *
+'c': no  : *  : *
+'d': no  : *  : *
+'e': no  : *  : *
+'f': no  : *  : *
+'g': no  : *  : *
+'h': no  : *  : *
+'i': no  : *  : *
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': no  : *  : *
+'B': no  : *  : *
+'C': no  : *  : *
+'D': no  : *  : *
+'E': no  : *  : *
+'F': no  : *  : *
+'G': no  : *  : *
+'H': no  : *  : *
+'I': no  : *  : *
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== testing hex digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': no  : *  : *
+'h': no  : *  : *
+'i': no  : *  : *
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': no  : *  : *
+'H': no  : *  : *
+'I': no  : *  : *
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-2 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': no  : *  : *
+'3': no  : *  : *
+'4': no  : *  : *
+'5': no  : *  : *
+'6': no  : *  : *
+'7': no  : *  : *
+'8': no  : *  : *
+'9': no  : *  : *
+'a': no  : *  : *
+'b': no  : *  : *
+'c': no  : *  : *
+'d': no  : *  : *
+'e': no  : *  : *
+'f': no  : *  : *
+'g': no  : *  : *
+'h': no  : *  : *
+'i': no  : *  : *
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': no  : *  : *
+'B': no  : *  : *
+'C': no  : *  : *
+'D': no  : *  : *
+'E': no  : *  : *
+'F': no  : *  : *
+'G': no  : *  : *
+'H': no  : *  : *
+'I': no  : *  : *
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-3 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': no  : *  : *
+'4': no  : *  : *
+'5': no  : *  : *
+'6': no  : *  : *
+'7': no  : *  : *
+'8': no  : *  : *
+'9': no  : *  : *
+'a': no  : *  : *
+'b': no  : *  : *
+'c': no  : *  : *
+'d': no  : *  : *
+'e': no  : *  : *
+'f': no  : *  : *
+'g': no  : *  : *
+'h': no  : *  : *
+'i': no  : *  : *
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': no  : *  : *
+'B': no  : *  : *
+'C': no  : *  : *
+'D': no  : *  : *
+'E': no  : *  : *
+'F': no  : *  : *
+'G': no  : *  : *
+'H': no  : *  : *
+'I': no  : *  : *
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-4 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': no  : *  : *
+'5': no  : *  : *
+'6': no  : *  : *
+'7': no  : *  : *
+'8': no  : *  : *
+'9': no  : *  : *
+'a': no  : *  : *
+'b': no  : *  : *
+'c': no  : *  : *
+'d': no  : *  : *
+'e': no  : *  : *
+'f': no  : *  : *
+'g': no  : *  : *
+'h': no  : *  : *
+'i': no  : *  : *
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': no  : *  : *
+'B': no  : *  : *
+'C': no  : *  : *
+'D': no  : *  : *
+'E': no  : *  : *
+'F': no  : *  : *
+'G': no  : *  : *
+'H': no  : *  : *
+'I': no  : *  : *
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-5 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': no  : *  : *
+'6': no  : *  : *
+'7': no  : *  : *
+'8': no  : *  : *
+'9': no  : *  : *
+'a': no  : *  : *
+'b': no  : *  : *
+'c': no  : *  : *
+'d': no  : *  : *
+'e': no  : *  : *
+'f': no  : *  : *
+'g': no  : *  : *
+'h': no  : *  : *
+'i': no  : *  : *
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': no  : *  : *
+'B': no  : *  : *
+'C': no  : *  : *
+'D': no  : *  : *
+'E': no  : *  : *
+'F': no  : *  : *
+'G': no  : *  : *
+'H': no  : *  : *
+'I': no  : *  : *
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-6 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': no  : *  : *
+'7': no  : *  : *
+'8': no  : *  : *
+'9': no  : *  : *
+'a': no  : *  : *
+'b': no  : *  : *
+'c': no  : *  : *
+'d': no  : *  : *
+'e': no  : *  : *
+'f': no  : *  : *
+'g': no  : *  : *
+'h': no  : *  : *
+'i': no  : *  : *
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': no  : *  : *
+'B': no  : *  : *
+'C': no  : *  : *
+'D': no  : *  : *
+'E': no  : *  : *
+'F': no  : *  : *
+'G': no  : *  : *
+'H': no  : *  : *
+'I': no  : *  : *
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-7 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': no  : *  : *
+'8': no  : *  : *
+'9': no  : *  : *
+'a': no  : *  : *
+'b': no  : *  : *
+'c': no  : *  : *
+'d': no  : *  : *
+'e': no  : *  : *
+'f': no  : *  : *
+'g': no  : *  : *
+'h': no  : *  : *
+'i': no  : *  : *
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': no  : *  : *
+'B': no  : *  : *
+'C': no  : *  : *
+'D': no  : *  : *
+'E': no  : *  : *
+'F': no  : *  : *
+'G': no  : *  : *
+'H': no  : *  : *
+'I': no  : *  : *
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-8 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': no  : *  : *
+'9': no  : *  : *
+'a': no  : *  : *
+'b': no  : *  : *
+'c': no  : *  : *
+'d': no  : *  : *
+'e': no  : *  : *
+'f': no  : *  : *
+'g': no  : *  : *
+'h': no  : *  : *
+'i': no  : *  : *
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': no  : *  : *
+'B': no  : *  : *
+'C': no  : *  : *
+'D': no  : *  : *
+'E': no  : *  : *
+'F': no  : *  : *
+'G': no  : *  : *
+'H': no  : *  : *
+'I': no  : *  : *
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-9 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': no  : *  : *
+'a': no  : *  : *
+'b': no  : *  : *
+'c': no  : *  : *
+'d': no  : *  : *
+'e': no  : *  : *
+'f': no  : *  : *
+'g': no  : *  : *
+'h': no  : *  : *
+'i': no  : *  : *
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': no  : *  : *
+'B': no  : *  : *
+'C': no  : *  : *
+'D': no  : *  : *
+'E': no  : *  : *
+'F': no  : *  : *
+'G': no  : *  : *
+'H': no  : *  : *
+'I': no  : *  : *
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-10 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': no  : *  : *
+'b': no  : *  : *
+'c': no  : *  : *
+'d': no  : *  : *
+'e': no  : *  : *
+'f': no  : *  : *
+'g': no  : *  : *
+'h': no  : *  : *
+'i': no  : *  : *
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': no  : *  : *
+'B': no  : *  : *
+'C': no  : *  : *
+'D': no  : *  : *
+'E': no  : *  : *
+'F': no  : *  : *
+'G': no  : *  : *
+'H': no  : *  : *
+'I': no  : *  : *
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-11 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': no  : *  : *
+'c': no  : *  : *
+'d': no  : *  : *
+'e': no  : *  : *
+'f': no  : *  : *
+'g': no  : *  : *
+'h': no  : *  : *
+'i': no  : *  : *
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': no  : *  : *
+'C': no  : *  : *
+'D': no  : *  : *
+'E': no  : *  : *
+'F': no  : *  : *
+'G': no  : *  : *
+'H': no  : *  : *
+'I': no  : *  : *
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-12 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': no  : *  : *
+'d': no  : *  : *
+'e': no  : *  : *
+'f': no  : *  : *
+'g': no  : *  : *
+'h': no  : *  : *
+'i': no  : *  : *
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': no  : *  : *
+'D': no  : *  : *
+'E': no  : *  : *
+'F': no  : *  : *
+'G': no  : *  : *
+'H': no  : *  : *
+'I': no  : *  : *
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-13 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': no  : *  : *
+'e': no  : *  : *
+'f': no  : *  : *
+'g': no  : *  : *
+'h': no  : *  : *
+'i': no  : *  : *
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': no  : *  : *
+'E': no  : *  : *
+'F': no  : *  : *
+'G': no  : *  : *
+'H': no  : *  : *
+'I': no  : *  : *
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-14 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': no  : *  : *
+'f': no  : *  : *
+'g': no  : *  : *
+'h': no  : *  : *
+'i': no  : *  : *
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': no  : *  : *
+'F': no  : *  : *
+'G': no  : *  : *
+'H': no  : *  : *
+'I': no  : *  : *
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-15 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': no  : *  : *
+'g': no  : *  : *
+'h': no  : *  : *
+'i': no  : *  : *
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': no  : *  : *
+'G': no  : *  : *
+'H': no  : *  : *
+'I': no  : *  : *
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-16 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': no  : *  : *
+'h': no  : *  : *
+'i': no  : *  : *
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': no  : *  : *
+'H': no  : *  : *
+'I': no  : *  : *
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-17 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': yes : 16 : 'G'
+'h': no  : *  : *
+'i': no  : *  : *
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': yes : 16 : 'G'
+'H': no  : *  : *
+'I': no  : *  : *
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-18 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': yes : 16 : 'G'
+'h': yes : 17 : 'H'
+'i': no  : *  : *
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': yes : 16 : 'G'
+'H': yes : 17 : 'H'
+'I': no  : *  : *
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-19 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': yes : 16 : 'G'
+'h': yes : 17 : 'H'
+'i': yes : 18 : 'I'
+'j': no  : *  : *
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': yes : 16 : 'G'
+'H': yes : 17 : 'H'
+'I': yes : 18 : 'I'
+'J': no  : *  : *
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-20 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': yes : 16 : 'G'
+'h': yes : 17 : 'H'
+'i': yes : 18 : 'I'
+'j': yes : 19 : 'J'
+'k': no  : *  : *
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': yes : 16 : 'G'
+'H': yes : 17 : 'H'
+'I': yes : 18 : 'I'
+'J': yes : 19 : 'J'
+'K': no  : *  : *
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-21 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': yes : 16 : 'G'
+'h': yes : 17 : 'H'
+'i': yes : 18 : 'I'
+'j': yes : 19 : 'J'
+'k': yes : 20 : 'K'
+'l': no  : *  : *
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': yes : 16 : 'G'
+'H': yes : 17 : 'H'
+'I': yes : 18 : 'I'
+'J': yes : 19 : 'J'
+'K': yes : 20 : 'K'
+'L': no  : *  : *
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-22 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': yes : 16 : 'G'
+'h': yes : 17 : 'H'
+'i': yes : 18 : 'I'
+'j': yes : 19 : 'J'
+'k': yes : 20 : 'K'
+'l': yes : 21 : 'L'
+'m': no  : *  : *
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': yes : 16 : 'G'
+'H': yes : 17 : 'H'
+'I': yes : 18 : 'I'
+'J': yes : 19 : 'J'
+'K': yes : 20 : 'K'
+'L': yes : 21 : 'L'
+'M': no  : *  : *
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-23 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': yes : 16 : 'G'
+'h': yes : 17 : 'H'
+'i': yes : 18 : 'I'
+'j': yes : 19 : 'J'
+'k': yes : 20 : 'K'
+'l': yes : 21 : 'L'
+'m': yes : 22 : 'M'
+'n': no  : *  : *
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': yes : 16 : 'G'
+'H': yes : 17 : 'H'
+'I': yes : 18 : 'I'
+'J': yes : 19 : 'J'
+'K': yes : 20 : 'K'
+'L': yes : 21 : 'L'
+'M': yes : 22 : 'M'
+'N': no  : *  : *
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-24 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': yes : 16 : 'G'
+'h': yes : 17 : 'H'
+'i': yes : 18 : 'I'
+'j': yes : 19 : 'J'
+'k': yes : 20 : 'K'
+'l': yes : 21 : 'L'
+'m': yes : 22 : 'M'
+'n': yes : 23 : 'N'
+'o': no  : *  : *
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': yes : 16 : 'G'
+'H': yes : 17 : 'H'
+'I': yes : 18 : 'I'
+'J': yes : 19 : 'J'
+'K': yes : 20 : 'K'
+'L': yes : 21 : 'L'
+'M': yes : 22 : 'M'
+'N': yes : 23 : 'N'
+'O': no  : *  : *
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-25 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': yes : 16 : 'G'
+'h': yes : 17 : 'H'
+'i': yes : 18 : 'I'
+'j': yes : 19 : 'J'
+'k': yes : 20 : 'K'
+'l': yes : 21 : 'L'
+'m': yes : 22 : 'M'
+'n': yes : 23 : 'N'
+'o': yes : 24 : 'O'
+'p': no  : *  : *
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': yes : 16 : 'G'
+'H': yes : 17 : 'H'
+'I': yes : 18 : 'I'
+'J': yes : 19 : 'J'
+'K': yes : 20 : 'K'
+'L': yes : 21 : 'L'
+'M': yes : 22 : 'M'
+'N': yes : 23 : 'N'
+'O': yes : 24 : 'O'
+'P': no  : *  : *
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-26 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': yes : 16 : 'G'
+'h': yes : 17 : 'H'
+'i': yes : 18 : 'I'
+'j': yes : 19 : 'J'
+'k': yes : 20 : 'K'
+'l': yes : 21 : 'L'
+'m': yes : 22 : 'M'
+'n': yes : 23 : 'N'
+'o': yes : 24 : 'O'
+'p': yes : 25 : 'P'
+'q': no  : *  : *
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': yes : 16 : 'G'
+'H': yes : 17 : 'H'
+'I': yes : 18 : 'I'
+'J': yes : 19 : 'J'
+'K': yes : 20 : 'K'
+'L': yes : 21 : 'L'
+'M': yes : 22 : 'M'
+'N': yes : 23 : 'N'
+'O': yes : 24 : 'O'
+'P': yes : 25 : 'P'
+'Q': no  : *  : *
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-27 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': yes : 16 : 'G'
+'h': yes : 17 : 'H'
+'i': yes : 18 : 'I'
+'j': yes : 19 : 'J'
+'k': yes : 20 : 'K'
+'l': yes : 21 : 'L'
+'m': yes : 22 : 'M'
+'n': yes : 23 : 'N'
+'o': yes : 24 : 'O'
+'p': yes : 25 : 'P'
+'q': yes : 26 : 'Q'
+'r': no  : *  : *
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': yes : 16 : 'G'
+'H': yes : 17 : 'H'
+'I': yes : 18 : 'I'
+'J': yes : 19 : 'J'
+'K': yes : 20 : 'K'
+'L': yes : 21 : 'L'
+'M': yes : 22 : 'M'
+'N': yes : 23 : 'N'
+'O': yes : 24 : 'O'
+'P': yes : 25 : 'P'
+'Q': yes : 26 : 'Q'
+'R': no  : *  : *
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-28 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': yes : 16 : 'G'
+'h': yes : 17 : 'H'
+'i': yes : 18 : 'I'
+'j': yes : 19 : 'J'
+'k': yes : 20 : 'K'
+'l': yes : 21 : 'L'
+'m': yes : 22 : 'M'
+'n': yes : 23 : 'N'
+'o': yes : 24 : 'O'
+'p': yes : 25 : 'P'
+'q': yes : 26 : 'Q'
+'r': yes : 27 : 'R'
+'s': no  : *  : *
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': yes : 16 : 'G'
+'H': yes : 17 : 'H'
+'I': yes : 18 : 'I'
+'J': yes : 19 : 'J'
+'K': yes : 20 : 'K'
+'L': yes : 21 : 'L'
+'M': yes : 22 : 'M'
+'N': yes : 23 : 'N'
+'O': yes : 24 : 'O'
+'P': yes : 25 : 'P'
+'Q': yes : 26 : 'Q'
+'R': yes : 27 : 'R'
+'S': no  : *  : *
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-29 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': yes : 16 : 'G'
+'h': yes : 17 : 'H'
+'i': yes : 18 : 'I'
+'j': yes : 19 : 'J'
+'k': yes : 20 : 'K'
+'l': yes : 21 : 'L'
+'m': yes : 22 : 'M'
+'n': yes : 23 : 'N'
+'o': yes : 24 : 'O'
+'p': yes : 25 : 'P'
+'q': yes : 26 : 'Q'
+'r': yes : 27 : 'R'
+'s': yes : 28 : 'S'
+'t': no  : *  : *
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': yes : 16 : 'G'
+'H': yes : 17 : 'H'
+'I': yes : 18 : 'I'
+'J': yes : 19 : 'J'
+'K': yes : 20 : 'K'
+'L': yes : 21 : 'L'
+'M': yes : 22 : 'M'
+'N': yes : 23 : 'N'
+'O': yes : 24 : 'O'
+'P': yes : 25 : 'P'
+'Q': yes : 26 : 'Q'
+'R': yes : 27 : 'R'
+'S': yes : 28 : 'S'
+'T': no  : *  : *
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-30 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': yes : 16 : 'G'
+'h': yes : 17 : 'H'
+'i': yes : 18 : 'I'
+'j': yes : 19 : 'J'
+'k': yes : 20 : 'K'
+'l': yes : 21 : 'L'
+'m': yes : 22 : 'M'
+'n': yes : 23 : 'N'
+'o': yes : 24 : 'O'
+'p': yes : 25 : 'P'
+'q': yes : 26 : 'Q'
+'r': yes : 27 : 'R'
+'s': yes : 28 : 'S'
+'t': yes : 29 : 'T'
+'u': no  : *  : *
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': yes : 16 : 'G'
+'H': yes : 17 : 'H'
+'I': yes : 18 : 'I'
+'J': yes : 19 : 'J'
+'K': yes : 20 : 'K'
+'L': yes : 21 : 'L'
+'M': yes : 22 : 'M'
+'N': yes : 23 : 'N'
+'O': yes : 24 : 'O'
+'P': yes : 25 : 'P'
+'Q': yes : 26 : 'Q'
+'R': yes : 27 : 'R'
+'S': yes : 28 : 'S'
+'T': yes : 29 : 'T'
+'U': no  : *  : *
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-31 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': yes : 16 : 'G'
+'h': yes : 17 : 'H'
+'i': yes : 18 : 'I'
+'j': yes : 19 : 'J'
+'k': yes : 20 : 'K'
+'l': yes : 21 : 'L'
+'m': yes : 22 : 'M'
+'n': yes : 23 : 'N'
+'o': yes : 24 : 'O'
+'p': yes : 25 : 'P'
+'q': yes : 26 : 'Q'
+'r': yes : 27 : 'R'
+'s': yes : 28 : 'S'
+'t': yes : 29 : 'T'
+'u': yes : 30 : 'U'
+'v': no  : *  : *
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': yes : 16 : 'G'
+'H': yes : 17 : 'H'
+'I': yes : 18 : 'I'
+'J': yes : 19 : 'J'
+'K': yes : 20 : 'K'
+'L': yes : 21 : 'L'
+'M': yes : 22 : 'M'
+'N': yes : 23 : 'N'
+'O': yes : 24 : 'O'
+'P': yes : 25 : 'P'
+'Q': yes : 26 : 'Q'
+'R': yes : 27 : 'R'
+'S': yes : 28 : 'S'
+'T': yes : 29 : 'T'
+'U': yes : 30 : 'U'
+'V': no  : *  : *
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-32 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': yes : 16 : 'G'
+'h': yes : 17 : 'H'
+'i': yes : 18 : 'I'
+'j': yes : 19 : 'J'
+'k': yes : 20 : 'K'
+'l': yes : 21 : 'L'
+'m': yes : 22 : 'M'
+'n': yes : 23 : 'N'
+'o': yes : 24 : 'O'
+'p': yes : 25 : 'P'
+'q': yes : 26 : 'Q'
+'r': yes : 27 : 'R'
+'s': yes : 28 : 'S'
+'t': yes : 29 : 'T'
+'u': yes : 30 : 'U'
+'v': yes : 31 : 'V'
+'w': no  : *  : *
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': yes : 16 : 'G'
+'H': yes : 17 : 'H'
+'I': yes : 18 : 'I'
+'J': yes : 19 : 'J'
+'K': yes : 20 : 'K'
+'L': yes : 21 : 'L'
+'M': yes : 22 : 'M'
+'N': yes : 23 : 'N'
+'O': yes : 24 : 'O'
+'P': yes : 25 : 'P'
+'Q': yes : 26 : 'Q'
+'R': yes : 27 : 'R'
+'S': yes : 28 : 'S'
+'T': yes : 29 : 'T'
+'U': yes : 30 : 'U'
+'V': yes : 31 : 'V'
+'W': no  : *  : *
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-33 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': yes : 16 : 'G'
+'h': yes : 17 : 'H'
+'i': yes : 18 : 'I'
+'j': yes : 19 : 'J'
+'k': yes : 20 : 'K'
+'l': yes : 21 : 'L'
+'m': yes : 22 : 'M'
+'n': yes : 23 : 'N'
+'o': yes : 24 : 'O'
+'p': yes : 25 : 'P'
+'q': yes : 26 : 'Q'
+'r': yes : 27 : 'R'
+'s': yes : 28 : 'S'
+'t': yes : 29 : 'T'
+'u': yes : 30 : 'U'
+'v': yes : 31 : 'V'
+'w': yes : 32 : 'W'
+'x': no  : *  : *
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': yes : 16 : 'G'
+'H': yes : 17 : 'H'
+'I': yes : 18 : 'I'
+'J': yes : 19 : 'J'
+'K': yes : 20 : 'K'
+'L': yes : 21 : 'L'
+'M': yes : 22 : 'M'
+'N': yes : 23 : 'N'
+'O': yes : 24 : 'O'
+'P': yes : 25 : 'P'
+'Q': yes : 26 : 'Q'
+'R': yes : 27 : 'R'
+'S': yes : 28 : 'S'
+'T': yes : 29 : 'T'
+'U': yes : 30 : 'U'
+'V': yes : 31 : 'V'
+'W': yes : 32 : 'W'
+'X': no  : *  : *
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-34 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': yes : 16 : 'G'
+'h': yes : 17 : 'H'
+'i': yes : 18 : 'I'
+'j': yes : 19 : 'J'
+'k': yes : 20 : 'K'
+'l': yes : 21 : 'L'
+'m': yes : 22 : 'M'
+'n': yes : 23 : 'N'
+'o': yes : 24 : 'O'
+'p': yes : 25 : 'P'
+'q': yes : 26 : 'Q'
+'r': yes : 27 : 'R'
+'s': yes : 28 : 'S'
+'t': yes : 29 : 'T'
+'u': yes : 30 : 'U'
+'v': yes : 31 : 'V'
+'w': yes : 32 : 'W'
+'x': yes : 33 : 'X'
+'y': no  : *  : *
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': yes : 16 : 'G'
+'H': yes : 17 : 'H'
+'I': yes : 18 : 'I'
+'J': yes : 19 : 'J'
+'K': yes : 20 : 'K'
+'L': yes : 21 : 'L'
+'M': yes : 22 : 'M'
+'N': yes : 23 : 'N'
+'O': yes : 24 : 'O'
+'P': yes : 25 : 'P'
+'Q': yes : 26 : 'Q'
+'R': yes : 27 : 'R'
+'S': yes : 28 : 'S'
+'T': yes : 29 : 'T'
+'U': yes : 30 : 'U'
+'V': yes : 31 : 'V'
+'W': yes : 32 : 'W'
+'X': yes : 33 : 'X'
+'Y': no  : *  : *
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-35 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': yes : 16 : 'G'
+'h': yes : 17 : 'H'
+'i': yes : 18 : 'I'
+'j': yes : 19 : 'J'
+'k': yes : 20 : 'K'
+'l': yes : 21 : 'L'
+'m': yes : 22 : 'M'
+'n': yes : 23 : 'N'
+'o': yes : 24 : 'O'
+'p': yes : 25 : 'P'
+'q': yes : 26 : 'Q'
+'r': yes : 27 : 'R'
+'s': yes : 28 : 'S'
+'t': yes : 29 : 'T'
+'u': yes : 30 : 'U'
+'v': yes : 31 : 'V'
+'w': yes : 32 : 'W'
+'x': yes : 33 : 'X'
+'y': yes : 34 : 'Y'
+'z': no  : *  : *
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': yes : 16 : 'G'
+'H': yes : 17 : 'H'
+'I': yes : 18 : 'I'
+'J': yes : 19 : 'J'
+'K': yes : 20 : 'K'
+'L': yes : 21 : 'L'
+'M': yes : 22 : 'M'
+'N': yes : 23 : 'N'
+'O': yes : 24 : 'O'
+'P': yes : 25 : 'P'
+'Q': yes : 26 : 'Q'
+'R': yes : 27 : 'R'
+'S': yes : 28 : 'S'
+'T': yes : 29 : 'T'
+'U': yes : 30 : 'U'
+'V': yes : 31 : 'V'
+'W': yes : 32 : 'W'
+'X': yes : 33 : 'X'
+'Y': yes : 34 : 'Y'
+'Z': no  : *  : *
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
+==== Testing base-36 digits ====
+'0': yes : 0 : '0'
+'1': yes : 1 : '1'
+'2': yes : 2 : '2'
+'3': yes : 3 : '3'
+'4': yes : 4 : '4'
+'5': yes : 5 : '5'
+'6': yes : 6 : '6'
+'7': yes : 7 : '7'
+'8': yes : 8 : '8'
+'9': yes : 9 : '9'
+'a': yes : 10 : 'A'
+'b': yes : 11 : 'B'
+'c': yes : 12 : 'C'
+'d': yes : 13 : 'D'
+'e': yes : 14 : 'E'
+'f': yes : 15 : 'F'
+'g': yes : 16 : 'G'
+'h': yes : 17 : 'H'
+'i': yes : 18 : 'I'
+'j': yes : 19 : 'J'
+'k': yes : 20 : 'K'
+'l': yes : 21 : 'L'
+'m': yes : 22 : 'M'
+'n': yes : 23 : 'N'
+'o': yes : 24 : 'O'
+'p': yes : 25 : 'P'
+'q': yes : 26 : 'Q'
+'r': yes : 27 : 'R'
+'s': yes : 28 : 'S'
+'t': yes : 29 : 'T'
+'u': yes : 30 : 'U'
+'v': yes : 31 : 'V'
+'w': yes : 32 : 'W'
+'x': yes : 33 : 'X'
+'y': yes : 34 : 'Y'
+'z': yes : 35 : 'Z'
+'A': yes : 10 : 'A'
+'B': yes : 11 : 'B'
+'C': yes : 12 : 'C'
+'D': yes : 13 : 'D'
+'E': yes : 14 : 'E'
+'F': yes : 15 : 'F'
+'G': yes : 16 : 'G'
+'H': yes : 17 : 'H'
+'I': yes : 18 : 'I'
+'J': yes : 19 : 'J'
+'K': yes : 20 : 'K'
+'L': yes : 21 : 'L'
+'M': yes : 22 : 'M'
+'N': yes : 23 : 'N'
+'O': yes : 24 : 'O'
+'P': yes : 25 : 'P'
+'Q': yes : 26 : 'Q'
+'R': yes : 27 : 'R'
+'S': yes : 28 : 'S'
+'T': yes : 29 : 'T'
+'U': yes : 30 : 'U'
+'V': yes : 31 : 'V'
+'W': yes : 32 : 'W'
+'X': yes : 33 : 'X'
+'Y': yes : 34 : 'Y'
+'Z': yes : 35 : 'Z'
+'!': no  : *  : *
+'@': no  : *  : *
+'?': no  : *  : *
diff --git a/tests/hard_coded/test_char_digits.m b/tests/hard_coded/test_char_digits.m
new file mode 100644
index 0000000..26bd592
--- /dev/null
+++ b/tests/hard_coded/test_char_digits.m
@@ -0,0 +1,90 @@
+%-----------------------------------------------------------------------------%
+% vim: ft=mercury ts=4 sw=4 et wm=0 tw=0
+%-----------------------------------------------------------------------------%
+%
+% Test the predicates for classifying various kinds of digits as well
+% as conversion from digits to integers and back again.
+%
+%-----------------------------------------------------------------------------%
+
+:- module test_char_digits.
+:- interface.
+
+:- import_module io.
+
+:- pred main(io::di, io::uo) is det.
+
+:- implementation.
+
+:- import_module char.
+:- import_module int.
+:- import_module list.
+:- import_module string.
+
+main(!IO) :-
+    do_test("Testing binary digits", is_binary_digit, binary_digit_to_int,
+        int_to_binary_digit, !IO),
+    do_test("Testing octal digits", is_octal_digit, octal_digit_to_int,
+        int_to_octal_digit, !IO),
+    do_test("Testing decimal digits", is_decimal_digit, decimal_digit_to_int,
+        int_to_decimal_digit, !IO),
+    do_test("testing hex digits", is_hex_digit, hex_digit_to_int,
+        int_to_hex_digit, !IO),
+    int.fold_up(test_base, 2, 36, !IO).
+
+:- pred do_test(
+    string::in,
+    pred(char)::in(pred(in) is semidet),
+    pred(char, int)::in(pred(in, out) is semidet),
+    pred(int, char)::in(pred(in, out) is semidet),
+    io::di, io::uo) is det.
+
+do_test(Header, IsDigit, ToInt, FromInt, !IO) :-
+    io.format("==== %s ====\n", [s(Header)], !IO),
+    list.foldl(do_test_char(IsDigit, ToInt, FromInt), digits, !IO).
+
+:- pred do_test_char(
+    pred(char)::in(pred(in) is semidet),
+    pred(char, int)::in(pred(in, out) is semidet),
+    pred(int, char)::in(pred(in, out) is semidet),
+    char::in, io::di, io::uo) is det.
+
+do_test_char(IsDigit, ToInt, FromInt, Char, !IO) :-
+    io.format("'%c': ", [c(Char)], !IO),
+    ( if IsDigit(Char)
+    then io.write_string("yes : ", !IO)
+    else io.write_string("no  : ", !IO)
+    ),
+    ( if ToInt(Char, Int) then
+        io.format("%d : ", [i(Int)], !IO),
+        ( if FromInt(Int, CharPrime) then
+            io.format("'%c'", [c(CharPrime)], !IO)
+        else
+            io.write_string("*", !IO)
+        )
+    else
+        io.write_string("*  : *", !IO)
+    ),
+    io.nl(!IO).
+
+:- pred test_base(int::in, io::di, io::uo) is det.
+
+test_base(Base, !IO) :-
+    string.format("Testing base-%d digits", [i(Base)], Header),
+    do_test(Header, is_base_digit(Base), base_digit_to_int(Base),
+        base_int_to_digit(Base), !IO).
+
+:- func digits = list(char).
+
+digits = [
+     '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+     'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
+     'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
+     'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D',
+     'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
+     'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
+     'Y', 'Z', '!', '@', '?'].
+
+%-----------------------------------------------------------------------------%
+:- end_module test_char_digits.
+%-----------------------------------------------------------------------------%



More information about the reviews mailing list