[comp.lang.ada] Calendar_Utilities package

emery@linus.mitre.org (David Emery) (07/06/90)

Here's a package that might prove useful in doing those tricky date
calculations in Ada.  As usual, if any bugs in this code are caught or
captured, I disavow any knowledge of (and responsibility for) their
actions. 

				dave emery
				emery@aries.mitre.org
---------------
with Calendar;
package Calendar_Utilities is

  subtype hours_number is integer range 0..23;
  subtype minutes_number is integer range 0..59;
  subtype seconds_number is integer range 0..59;
  subtype decimal_seconds is duration range duration'(0.0) .. duration'(1.0);

  -- date strings are of the form 
  -- year/mo/da hr:mn:sc.decimal
  
  function image_of (time : Calendar.time)
      return string;

  function value_of (str : string)
      return Calendar.time;
      
  function image_size (time : Calendar.time)
      return natural;

  function years_in (time : Calendar.time)
      return Calendar.year_number;
	  
  function months_in (time : Calendar.time)
      return Calendar.month_number;
	  
  function days_in (time : Calendar.time)
      return Calendar.day_number;
	  
  function seconds_in (time : Calendar.time)
      return Calendar.day_duration;

  -- duration strings are of the form 
  -- hr:mn:sc.decimal
      
  function image_of (d : duration)
      return string;
  
  function value_of (str : string)
      return duration;
      
  function image_size (d : duration)
      return natural;

  procedure split (d   : in     duration;
  		   hr  :    out hours_number; 
		   min :    out minutes_number;
		   sec :    out seconds_number;
		   dec :    out decimal_seconds);

  function duration_of (hrs : hours_number;
  			min : minutes_number;
			sec : seconds_number;
			dec : decimal_seconds)
      return duration;			

  function hours_in (d : duration)
      return hours_number;
      
  function minutes_in (d : duration)
      return minutes_number;
      
  function seconds_in (d : duration)
      return seconds_number;
      
  function decimal_seconds_in (d : duration)
      return decimal_seconds;

  function clock_string
      return string;	-- is image_of(Calendar.clock);

end Calendar_Utilities;

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

with Text_IO;
package body Calendar_Utilities is

  use Calendar;
  
  function two_digit_image (n : natural)
      return string;
  pragma inline (two_digit_image);

  function to_integer (d : duration)
      return integer;
  pragma inline (to_integer);

  no_number_found : exception;  

  procedure get_next_number
	(str : string;
	 start : natural;
	 value : out integer;
	 next_char : out natural);
  pragma inline (get_next_number);
  
  function image_of (time : Calendar.time)
      return string
  is
    year : year_number;
    month : month_number;
    day : day_number;
    seconds : day_duration;
  begin
    split (time, year, month, day, seconds);
    return integer'image(year)(2..5)
	   & '/' & two_digit_image(month)
	   & '/' & two_digit_image(day)
	   & ' ' & image_of(seconds);
  end image_of;
  
  function value_of (str : string)
      return Calendar.time
  is
    answer : Calendar.time;
    year : year_number;
    month : month_number;
    day : day_number;
    seconds : day_duration;
    start : natural := str'first;
  begin		-- year/mo/da hr:mn:sc.decimal
    -- find year
    get_next_number (str, start, year, start);
    if (str(start) /= '/') then
      raise constraint_error;	-- format error
    else
      start := start + 1;
    end if;

    -- find month
    get_next_number (str, start, month, start);
    if (str(start) /= '/') then
      raise constraint_error;	-- format error
    else
      start := start + 1;
    end if;

    -- find day
    get_next_number (str, start, day, start);

    -- get seconds
    seconds := value_of (str(start..str'last));

    answer := Calendar.time_of (year, month, day, seconds);
    return answer;
  end value_of;
      
  function image_size (time : Calendar.time)
      return natural
  is
    year : year_number;
    month : month_number;
    day : day_number;
    seconds : day_duration;
  begin
    Calendar.split (time, year, month, day, seconds);
	-- year /   mo  /   da      hr:mn:sc.decimal 
    return 4 +  1 + 2 + 1 + 2 + 1 + image_size(seconds);
  end image_size;

  function years_in (time : Calendar.time)
      return Calendar.year_number
  is
    year : year_number;
    month : month_number;
    day : day_number;
    seconds : day_duration;
  begin
    Calendar.split (time, year, month, day, seconds);
    return year;
  end years_in;
	  
  function months_in (time : Calendar.time)
      return Calendar.month_number
  is
    year : year_number;
    month : month_number;
    day : day_number;
    seconds : day_duration;
  begin
    Calendar.split (time, year, month, day, seconds);
    return month;
  end months_in;
	  
  function days_in (time : Calendar.time)
      return Calendar.day_number
  is
    year : year_number;
    month : month_number;
    day : day_number;
    seconds : day_duration;
  begin
    Calendar.split (time, year, month, day, seconds);
    return day;
  end days_in;
  
  function seconds_in (time : Calendar.time)
      return Calendar.day_duration
  is
    year : year_number;
    month : month_number;
    day : day_number;
    seconds : day_duration;
  begin
    Calendar.split (time, year, month, day, seconds);
    return seconds;
  end seconds_in;

  function image_of (d : duration)
      return string
  is
    hr : hours_number;
    min : minutes_number;
    sec : seconds_number;
    dec : decimal_seconds;
    int_decimal : integer;
  begin
    split (d, hr, min, sec, dec);
    int_decimal := to_integer(dec);
    return two_digit_image(hr) 
    	   & ':' & two_digit_image(min)
	   & ':' & two_digit_image(sec)
	   & "." & integer'image(int_decimal)	-- get rid of leading space
			(2..integer'image(int_decimal)'last);
  end image_of;    
  
  function value_of (str : string)
      return duration
  is
    hr, min, sec, decimal : integer;
    start : integer := str'first;
    integer_seconds : integer;
    answer : duration;
  begin	  -- hr:mn:sc.decimal
    -- find hours
    get_next_number (str, start, hr, start);
    if (str(start) /= ':') then
      raise constraint_error;	-- format error
    else
      start := start + 1;
    end if;

    -- find min
    get_next_number (str, start, min, start);
    if (str(start) /= ':') then
      raise constraint_error;	-- format error
    else
      start := start + 1;
    end if;

    -- find sec
    get_next_number (str, start, sec, start);
    if (str(start) /= '.') then
      raise constraint_error;	-- format error
    else
      start := start + 1;
    end if;

    -- find decimal
    get_next_number (str, start, decimal, start);

    -- put it all together
    integer_seconds := hr * (60*60) + min *60 + sec;
    answer := duration(integer_seconds)
	      + duration(decimal * duration(duration'delta));
    return answer;
  end value_of;
      
  function image_size (d : duration)
      return natural
  is
    decimal_size : integer;
  begin -- hr:mn:sc.decimal
    decimal_size := integer'image
		(to_integer(decimal_seconds_in(d)))'length -1;
    return 2 + 1 + 2 +1 + 2 + 1
	   + decimal_size;
  end image_size; 

  procedure split (d   : in     duration;
  		   hr  :    out hours_number; 
		   min :    out minutes_number;
		   sec :    out seconds_number;
		   dec :    out decimal_seconds)
  is
    seconds : integer;
    hr_local, min_local : integer;
  begin
    seconds :=integer(d);
    if (duration(seconds) > d) then
	seconds := seconds -1;
    end if;
    dec := d - duration(seconds);
    hr_local:= seconds / (60*60);
    seconds := seconds - (hr_local *(60*60));
    min_local := seconds / 60;
    sec := seconds - (min_local*60);
    hr := hr_local; min := min_local;
  end split;
	
  function duration_of (hrs : hours_number;
  			min : minutes_number;
			sec : seconds_number;
			dec : decimal_seconds)
      return duration
  is
    seconds : integer;
  begin
    seconds := (hrs * (60*60)) + (min * 60) + sec;
    return duration(seconds) + dec;
  end duration_of;

  function hours_in (d : duration)
      return hours_number
  is
    answer : integer;
  begin
    answer := (integer(d) /(60*60));
    return hours_number(answer);
  end hours_in;
	
  function minutes_in (d : duration)
      return minutes_number
  is
    answer : integer;
  begin
    answer := (integer(d) - (hours_in(d) * 60 * 60)) /60;
    return minutes_number(answer);
  end minutes_in;
	
  function seconds_in (d : duration)
      return seconds_number
  is
    answer : integer;
  begin
    answer := (integer(d) - (hours_in(d) *60*60) - (minutes_in(d)* 60));
    return seconds_number(answer);
  end seconds_in;  
      
  function decimal_seconds_in (d : duration)
      return decimal_seconds
  is
  begin
    return decimal_seconds(d - duration(integer(d)));
  end decimal_seconds_in;  

  function clock_string
      return string	-- is image_of(Calendar.clock);
  is
  begin
    return image_of (calendar.clock);
  end clock_string;

  -- private bodies
  function two_digit_image (n : natural)
      return string
  is
  begin
    case n is
      when 0..9 =>
	return '0' & integer'image(n)(2);
      when 10..99 =>
	return integer'image(n)(2..3);
      when others =>
	raise constraint_error;
    end case;
  end two_digit_image;

  function to_integer (d : duration)
      return integer
  is
  begin
    return integer(d * duration(1.0/duration'delta));
  end to_integer;

  procedure get_next_number
	(str : string;
	 start : natural;
	 value : out integer;
	 next_char : out natural)
  is
    current : natural := start;
    answer : integer := 0;
    found_number : boolean := false;
  begin
    loop
      if (current > str'last) then
	exit;
      end if;
      case str(current) is
	when ' ' | Ascii.ht =>	
	  if found_number then	
	    exit;	-- terminates number
	    		-- otherwise skip
	  end if;
        when '0' .. '9' =>
	  found_number := true;
	  answer := answer * 10 +
			(character'pos(str(current)) - character'pos('0'));
	when '-' => 	-- negatives not allowed!!
	  raise constraint_error;
	when others =>
	  exit;
      end case;
      current := current + 1;
    end loop;
    if found_number then
      next_char := current;
      value := answer;
    else
      raise no_number_found;
    end if;
  end get_next_number;
  

end Calendar_Utilities;

mfeldman@seas.gwu.edu (Michael Feldman) (07/07/90)

Dave Emery's calendar utilities package is really nice. A quick scan of it 
reveals a small flaw that has bitten me and my students more than once: in
extracting the integer part of Calendar.Seconds(Calendar.Time) the package
is converting to predefined integer. This is not portable, because if it's
late in the day and integer'size = 16 (as is the case on PC's, usually),
the conversion will raise constraint_error. "Mainframe" Ada's generally use
32-bit integer, which is clearly bigger than 86400.0, but Meridian and
Janus both use 16-bit integer. Bad news here. My hack is to convert to
float first. I'm not sure what accuracy problems this might present if
you're working in fractions of a second, but you all get the idea.

Anybody got a cleaner hack than mine? Have a look at this code:
(obviously My_Int_IO is the usual instantiation for predefined integer).
I've tested on both Unix and PC Ada's.
(Please don't flame about the simple-minded repetitive calculations;
I broke it out so the novice could easily understand it).

WITH Text_IO;
WITH My_Int_IO;
WITH Calendar;
PROCEDURE TimeOfDay IS

  CurrentTime :      Calendar.Time;
  SecsPastMidnight : Calendar.Day_Duration;
  MinsPastMidnight : Natural;
  Secs :             Natural;
  Mins :             Natural;
  Hrs :              Natural;

BEGIN -- show time as hh:mm:ss, 24-hr time

  CurrentTime := Calendar.Clock;
  SecsPastMidnight := Calendar.Seconds(CurrentTime);
  MinsPastMidnight := Natural(Float(SecsPastMidnight)/60.0 - 0.5);
  Secs :=  Natural(Float(SecsPastMidnight) - 60.0 * Float(MinsPastMidnight));
  Mins :=             MinsPastMidnight REM 60;
  Hrs :=              MinsPastMidnight / 60;

  My_Int_IO.Put (Item => Hrs, Width => 1);
  Text_IO.Put (Item => ':');
  IF Mins < 10 THEN
     Text_IO.Put (Item => '0');
  END IF;
  My_Int_IO.Put (Item => Mins, Width => 1);
  Text_IO.Put (Item => ':');
  IF Secs < 10 THEN
     Text_IO.Put (Item => '0');
  END IF;
  My_Int_IO.Put (Item => Secs, Width => 1);
  Text_IO.New_Line;

END TimeOfDay;