No 'Truncation attribute for fixed points types

Hi, in Adacraft there is this piece of code which looks ugly, meant to bypass the lack of 'Truncation attribute for fixed point types:

    type Integer_Time is range 0 .. Seconds_Per_Day;

    function Convert_Time (Time : Day_Duration) return Integer_Time is
            type Extended_Integer_Time is Irange Integer_Time'First .. Integer_Time'Last + 1;
            T : Extended_Integer_Time := Extended_Integer_Time (Time);
    begin
           return Integer_Time (T mod Extended_Integer_Time'Last);
    end Convert_Time;

Can the more recent language revisions improve that ?

Presuming

  1. Seconds_Per_Day is 86_400
  2. Day_Duration is Ada.Calendar.Day_Duration
  3. the typo in the declaration of Extended_Integer_Time is corrected

then Convert_Time appears to me to be identical to

function Convert_Time (Time : Day_Duration) return Integer_Time is
   (Integer_Time (Time) );

I don’t know what problem Convert_Time is supposed to address, so I can’t suggest an alternative.

I usually work around the lack of 'Truncation for fixed-point types by doing

function Truncated (Value : in Fix) return Fix is
   type Flt is digits System.Max_Digits;
begin -- Truncated
   return Fix (Flt'Truncation (Flt (Value) ) );
end Truncated;

Here is the rationale. I do not claim to understand. Understanding algorithmics/mathematics for me is complicated, I always need concrete exemples to grok anything… or else it amounts to a fat word salad.

There’s one nasty little trap in the package body which I’ve carefully avoided. Since real values are converted to integers by rounding, Day_Duration is rounded to an integer in the range 0 to 86400. We actually want a value in the range 0 to 86399, so the result of the rounding needs to be taken modulo 86400. If I’d forgotten to do this the program would crash in the last half-second before midnight, but would work perfectly the rest of the time. Bugs like this can be quite hard to detect, since very few people do their debugging at exactly half a second before midnight!
I’ve defined the function Convert_Time to do the conversion from Day_Duration to Integer_Time. I’ve had to define an internal type called Extended_Integer_Time with a range of 0…86400 rather than 0…86399 to avoid constraint errors when rounding from Day_Duration. The mod operator is then used to produce a result in the range 0…86399 which is then converted to an Integer_Time result. Using a modular type for Integer_Time wouldn’t help since values aren’t forced into range when converting to a modular type; you’d still get a constraint error in the last half-second before midnight. If Duration were a floating point type, you could get around this problem by using the 'Truncation attribute (see Appendix C) to do the conversion by truncation instead of rounding. Unfortunately there is no 'Truncation attribute for fixed point types, which appears to be an oversight on the part of the language standards committee; there is no easy way to do fixed point truncation without using an integer type with a wider range than you actually require.

I’ve always used another fixed point type as an intermediate conversion since those truncate rather than round on conversion:

    function Truncate(Time : Day_Duration) return Integer_Time is
       type Truncator is delta 1.0 digits 5;
    begin
       return Integer_Time(Truncator(Time));
    end Truncate;

You still need to decide how to handle a Day_Duration value of 86_400 if you truly want 0 …86399, but that isn’t related to truncating specifically.

EDIT: changed to a decimal fixed point to make it compiler portable. Previous was gnat only.

This implies that Seconds_Per_Day is 86_399, making it poorly named. This is not actually truncating the result, since truncating 86_399.9 would give 86_399, and this returns 0. A clearer way to write this might be

function Convert_Time (Time : Day_Duration) return Integer_Time is
 (if Time >= 86_399.5 then 0 else Integer_Time (Time) );

For actual truncation, one has to decide what to do if Time is 86_400.0. If that should go to zero, then it would be

function Convert_Time (Time : Day_Duration) return Integer_Time is
 (if Time >= Day_Duration'Last then 0 else Integer_Time (Truncated (Time) ) );

where Truncated is as given above, but with Fix replaced by Day_Duration.

This is incorrect. Only decimal fixed-point types must truncate on conversion; the ARM does not specify the behavior for ordinary fixed_point types. GNAT truncates, but ObjectAda rounds.

That’s fine, then use a decimal fixed point.

function Convert_Time (Time : Day_Duration) return Integer_Time is
(if Time >= Day_Duration’Last then 0 else Integer_Time (Truncated (Time) ) );

Wait, how could Time be superior to its subtype’s upper bound without raising an exception ? This is precisely what the convoluted textbook exemple aimed to avoid.

Time is of type Day_Duration, so comparing Time to Day_Duration’Last is within its bounds.

Not the problem… Time of the subtype Day_Duration can not trigger that conditional, because its value cannot lay outside its subtype. Assuming the value would input that value manually, it would raise Constraint_Error before Convert_Time is called. That’s why you would need a kind of supertype… which 'Base can do ?!

procedure essai is
	type T is new Integer range 1..10;
	function call (A: T'Base) return T is (T(A mod T'Last));		
	B: T := call(11);
begin
	Put_line(B'Image);
end essai;

I had no idea 'Base could be used as a valid subtype ! Now that fixes the problem.
Yes it doesn’t explicit prove that T’Last + 1 does fit within T’Base’s range, so it’s not as good as a having declared a proper extended type, but in my case, I can’t imagine it wouldn’t fit.

1 Like

Awesome! I’m glad you discovered something that helps out!

Maybe we were talking about two different parts. I thought you meant the conditional:

if Time >= Day_Duration’Last then

Which can indeed be triggered by the a value of the variable named Time. If you meant another part then my apologies.



If you did mean that part, then please consider:

The values of Time include potentially 0.0 .. 86400.0 per the Ada standard. The value of Day_Duration'Last is 86400.0 per the Ada standard. It is not outside the bounds of the subtype.

So the conditional which uses greater than or equals to should catch the situation where Time is of value 86400.0:

Conditional: 
Time >= Day_Duration’Last

Time Value | Conditional Result
-------------------------------
0.0        | False (< Day_Duration'Last)
to         |
86399.9    | False (< Day_Duration'Last)
86400.0    | True  (= Day_Duration'Last)

Yes the greater than doesn’t make much sense here and is probably overkill, but he equals to part of the conditional still checks one value in range of the subtype, namely 86400.0.

Hopefully that clarifies it better?

What the… Sorry, I was being stupid. In my mind I confused the return type Integer_Time with Time’s type. Must be the heat, Portugal is over 30 every day now and it seriously gets to my head.

The heat is rough here too! I totally understand!

Yes, that works fine, though then you have to make sure you specify enough digits.

The important thing is that someone reading this be aware that this approach requires a decimal fixed-point type.

  1. (I think this is correct, but may be mistaken.) A fixed point type is represented by a set of model numbers, which are integer multiples of the type’s small. Values which are not exactly a model number are represented by the nearest model number. For an ordinary fixed-point type, such as Duration, the small by default is a power of two <= the delta. This sometimes leads to confusion, such as why
with Ada.Text_IO;

procedure Funny_Money is
   type Money is delta 0.01 range -1.0E-6 .. 1.0E6;

   Cash : Money := 0.0;
begin -- Funny_Money
   Sum_5 : for I in 1 .. 5 loop
      Cash := Cash + 0.01;
   end loop Sum_5;

   Ada.Text_IO.Put_Line (Item => Cash'Image);
end Funny_Money;

outputs 0.04.

It’s possible for the values given in the range of a fixed-point (sub)type declaration to not be model numbers of the type, in which case the (sub)type will include the nearest model number, which may be greater than the value given.

A compiler may use greater accuracy/precision to evaluate an expression than that of the expression’s operands. If it does so, then both the actual value and the model number used to represent it may be exactly represented, so that “=” for them might return False.

It’s possible (but unlikely) that 86_400.0 is not a model number of Duration. If the corresponding model number is greater than 86_400.0 and enhanced accuracy is used in evaluating the comparison, then it may be possible for the model number to evaluate as greater than 86_400.0.

To be honest, though, Day_Duration’Last is probably a model number, so this wouldn’t be an issue in this case.

  1. It’s a general rule that one should never use “=” for real values.
  2. Even if it’s not possible for Time to be greater than Day_Duration’Last, the comparison is still legal and and correct.
  3. This is a common outcome of copy-and-modify reuse.