File : shuffle_text_tokens.adb


------------------------------------------------------------------------------
--  Demonstration code to read a text, split it into tokens, and shuffle them.
------------------------------------------------------------------------------
--  This version does not pollute the stack, prevents heap memory leaks.      
------------------------------------------------------------------------------

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Strings.Maps, Ada.Strings.Fixed; use Ada.Strings;
with Ada.Strings.Unbounded;
with Ada.Numerics.Discrete_Random;
with Ada.Calendar;
with Unchecked_Deallocation;

procedure shuffle_text_tokens is
   
   type Token is record
      first, last : Positive;    --  Slice marker of an external string
      output : Boolean;          --  Tag to prevent double output of tokens
   end record;
   
   --  The list of all tokens is an simple array of all tokens.
   --  It's not possible to hold an array of Strings, because Strings are
   --  unconstrained.
   type Tokens is array (Positive range <>) of Token;
   
   --  The empty array required for iteration start.
   --  Even the array does not contain any elements, Ada requires a dummy
   --  definition structure.
   no_tokens : constant Tokens (1 .. 0) := (others => (1, 1, False));
   
   --  Read the whole text from standard input into a single string.
   --  Note, that the String size is returned, too!  You do not need
   --  to guess how large the array might be.
   function get_whole_text return String;
   
   --  Split a string into a array of tokens. Note, that the array size is
   --  returned, too! You do not need to guess how large the array might be.
   function split (line : String) return Tokens;

   --  Implementation of get_whole_text.
   --  A chunk of Characters is read at once from the current line.
   --  Get_Line uses two out parameters, but all constrained parameters are
   --  always "in" parameter for there own constraints. So the Get_Line
   --  procedure learns about the size of the chunk. Because the constraints
   --  als always "in", they can't be changed. Get_Line returns the really
   --  filled amount as a second out parameter, and fills the chunk partially.
   --  This chunk is added to a dynamic string. On EOF a LF is inserted.
   --  On EOT the Get_Line function raises End_Error which terminates the
   --  endless loop and returns the result. The dynamic string type is
   --  finalized on end of the routine giving back all the required memory.
   function get_whole_text return String is
      buff : String (1 .. 100);
      last : Natural;
      use Ada.Strings.Unbounded;
      res  : Unbounded_String := Null_Unbounded_String;
   begin
      loop
         Get_Line (buff, last);
         Append (res, buff (buff'First .. last));
         if last < buff'Last then
            Append (res, ASCII.LF);
         end if;
      end loop;
   exception
      when End_Error =>
         return To_String (res);
   end get_whole_text;
   
   --  Implementation of split.
   --  Scans the string for tokens using Ada.Strings.Fixed.Find_Token.
   --  The out parameter "last" is used a the iteration variable.
   --  In order to prevent head exhaustion, the allocated array is freed as
   --  soon as possible (remalloc would be more efficient). Automatic garbage
   --  colletion by limiting the pool size (Tokens_Access'Storage_Size) is
   --  not possible, because the token array can be arbitary large.
   function split (line : String) return Tokens is
      first : Positive;
      last  : Natural := line'First - 1;
      type Tokens_Access is access Tokens;
      procedure Free is new Unchecked_Deallocation (Tokens, Tokens_Access);
      res : Tokens_Access := new Tokens'(no_tokens);
      seperators : constant Maps.Character_Set :=
        Maps.To_Set (" .,:;?!" & ASCII.HT & ASCII.LF);
   begin
      loop
         Fixed.Find_Token (
           Source => line (last + 1 .. line'Last),
           Set    => seperators,
           Test   => Outside,
           First  => first,
           Last   => last
         );
         exit when last = 0;
         declare
            oldres : Tokens_Access := res;
         begin
            res := new Tokens'(oldres.all & Token'(first, last, False));
            Free (oldres);
         end;
      end loop;
      return res.all;
   end split;
   
   --  Read in the whole text into a variable which should be immutable.
   --  The required size (constraint) comes from the defining return value.
   text : constant String := get_whole_text;
   
   --  Split the whole text into an array of tokens.
   --  The required size (constraint) comes from the defining return value.
   toks : Tokens := split (text);
   
   --  Initialize a random generator for the offset range of the toks array.
   subtype Tokens_Range is Natural range toks'First .. toks'Last;
   package Tokens_Random is new Ada.Numerics.Discrete_Random (Tokens_Range);
   gen   : Tokens_Random.Generator;
   pos   : Tokens_Range;
   count : Tokens_Range;

   --  Start "real" work. A substantially amount of work is already done
   --  by initializing the variables "text" and "toks".
begin
   --  Initialize the Random Generator with the current time.
   Tokens_Random.Reset (gen,
     Integer (Ada.Calendar.Seconds (Ada.Calendar.Clock)));
   
   --  A hand coded loop with a delayed increment.
   --  Constraint Error is raised when no tokens are found at all.
   count := Tokens_Range'First;
   loop
      pos := Tokens_Random.Random (gen);
      if not toks (pos).output then
         Put (text (toks (pos).first .. toks (pos).last));
         toks (pos).output := True;
         count := Tokens_Range'Succ (count); -- Constraint_Error = Last Item
         Put (' ');
      end if;
   end loop;

exception
   when Constraint_Error =>
      New_Line;
end shuffle_text_tokens;