Modular arithmetic on arbitrary array bounds

Hi, I’m writing a “Rotate_right” routine and I can’t get the cicling right. So if an increment is 10 and the bounds are 2…8, I must arrive at 4.

I’m stuck at this:

pragma Overflow_Mode (minimized);
pragma Ada_2022;
with Ada.Text_IO; use Ada.Text_IO;

procedure main with Spark_Mode is
   type Buffer_type is array (Positive range <>) of Character;
   function Rotate_Right (Buffer: Buffer_Type; D: Natural) return Buffer_type is 
     ((for I in Buffer'Range =>
       Buffer ((if I + D <= Buffer'Last then I + D else
       Buffer'First + (D - Buffer'Last + I + 1) mod (Buffer'Length - 1)))));
        
begin
   for I in 0..20 loop
    put_line ("I " & " : " & Character'Val (9) & String (Rotate_Right ("je ne sais pas", I)));
   end loop;
end main;

it eats letters. I could get a version where it works fine… until D = Buffer’Length, where an index gets out of boud, but I don’t get which one.
Can you help me ?

if is not needed at all: buffer(buffer’first + (i-buffer’first + d) mod buffer’length).

3 Likes

This works for me:

   return Buffer_Type is
     (for I in Buffer'Range =>
       Buffer (if I + D <= Buffer'Last then I + D else
       Buffer'First + (I + D - Buffer'First) mod Buffer'Length));

(a loop or conditional expression doesn’t need an extra paren if it’s immediately inside one already)

x mod Buffer’Length, when Buffer’Length is 14, returns values in 0 … 13, which is correct for adding to Buffer’First.

I like to keep the common parts of expressions like these in the same order (i.e. the I + D).

By the way, GNATpp (v24, v25) can’t handle this source:

rotation.adb:1:1: Error formatting node (CompilationUnit). Keeping the initial input selection unchanged
null template: <IteratedAssoc rotation.adb:10:7-12:64>

(the body of Rotate_Right - I reformatted a bit because 80-character editor window).

1 Like

You should take care not to override the buffer with itself. And instead of slow by element copying you should copy array slices.

BTW, I don’t know the purpose of your exercise, but when implementing a ring buffer one rotates the index rather than the buffer:

`Buffer ((Index - Buffer’First + Shift) mod Buffer’Length + Buffer’First)

1 Like

@dkazankov’s solution still defeats GNATpp :frowning:

1 Like

Holy mother of bloody Christ.
Why can I waste hours on this trying all kinds of formulas while it was so simple ? What is it, just experience ? All those expressions feel like my early days of fighting the compiler when I didn’t know the grammar at all: I want to get somewhere, but can’t quite find the simplest way, which is usually not the one my mind comes up with.

How do you shift slices ?

1 Like

You need not. In Ada arrays of same type are congruent if they have same length. It is also OK to overlap, the compiler chooses the direction of copying that does not corrupt the source.

declare
   S : String := "1234567890";
begin
   S (2..5) := S (6..9);
   Put_Line (S);
   S (7..9) := S (8..10);
   Put_Line (S);
   S (1..3) := S (2..4);
   Put_Line (S);
end;

The output:

1678967890
1678968900
6788968900

With a subtype conversion:

type Arr is array (Positive range <>) of ...;
subtype Arr_15_19 is Arr (15 .. 19);
X: Arr (1 .. 80);
Arr_15_19 (X (6 .. 10))  -- this is not a copy, but behaves like a constant

This is the best way to change the bounds of an array (or part of an array) in Ada.

It throws constraint_error when you set the subtype’s 'First to 0 (to apply modular arithmetic) so not usable for my purpose:

pragma Ada_2022;
procedure main is
	function rotate (A: String; B: Natural) return String is
		subtype String_from_zero is String (0 .. A'Length-1);
		S renames String_from_zero (A);
	begin
		return (for I in S'Range => S ((I + B) mod S'Length));
	end rotate;
begin
	null;
end main;

Dmitry’s proposition is better and more aesthetic.

As I said, use slices. It is much simpler to understand and more efficient. BTW, I suggest pragma Ada_2005. That would preclude you from using indigestible new stuff like if- and for-expressions etc.

Anyway:

function Rotate_Right (A: String; B: Natural) return String is
begin
   if A'Length = 0 then -- Empty string, sanity check
      return A;
   else
      declare
         Offset : constant Natural := B mod A'Length;
      begin
         if Offset = 0 then
            return A;
         else
            return A (A'Last - Offset + 1..A'Last) &
                   A (A'First..A'Last - Offset);
         end if;
      end;
   end if;
end Rotate_Right;
1 Like

Huh, always the reactionary ! I would have a hard time proving anything without these ! And I wouldn’t have the pleasure to pervert your holy code:

function Rotate_Right (A: String; B: Natural) return String is
	(if A'Length = 0 then A else
		(declare Offset: constant Natural := B mod A'Length;
		begin (if Offset = 0 then A else A(A'Last - Offset + 1..A'Last) &
                   A (A'First..A'Last - Offset))));

Ahahah. Thanks, I was looking for this but I just couldn’t make the operation in my head. Makes sense now, no actual circling. And it’s more efficient, assuming I would need to entire pages at once or thousands of time per minute :slight_smile:

1 Like

There’s a couple reasons. First your subtype String_from_Zero is a subtype of String which only allows Positive ranges (so no 0 indexing). You would need to do a custom character array type if you want to force the 0 index. However, that won’t be enough on its own as: Second is that for whatever reason the compiler is trying to assign a 0 index to the result type (I don’t know if this is a compiler bug or a language rule).

You can mod that function to fix both issues:

    function rotate (A: String; B: Natural) return String is
	    type String_from_zero is array(0 .. A'Length-1) of Character;
	    S renames String_from_zero (A);
    begin
	    return [for I in 1 .. A'Length => S((I-1+B) mod A'Length)];
    end rotate;

That said you can do it a bit more simply:

function Rotate(A : String; B : Natural) return String is
   (for I in A'Range => A((I-A'First+B) mod A'Length + A'First));

The way it works is you subtract out the 'First offset from the I index since I can start with values other than 1, then add B, then do the mod math, then add back in the 'First offset to get it back to the same index range as A (the compiler is already trying to force this and it is causing a Constraint_Error otherwise). Note you don’t need to worry about 0 length strings here either since the for loop handles that

or if you want to force a starting index of 1 in the result:

function Rotate(A : String; B : Natural) return String is
   (for I in 1..A'Length => A((I-1+B) mod A'Length + A'First));

If you want to do it through slices:

     function Rotate(A : String; B : Natural) return String is
	    (if B = 0 or A'Length = 0 then 
	        A 
	     else 
	          A(A'First + (B mod A'Length) .. A'Last) 
            & A(A'First .. A'First + (B mod A'Length) - 1));
1 Like

Ah I finally see in my head what happens… Damn it took me time. I guess there’s nothing but practice to make those inner muscles, if as it happens, I don’t really have the mental imagination by default. Well I think not many people when it comes to modular arithmetic or combinatorics.

No problem. Just a note that I modded the slice version to handle empty strings and large B values as it didn’t have the logic to guard against those previously.

Ring buffers are fun. The really interesting stuff begins when you start to write variable length items into it (or rotate, considering your task, which I still believe is totally useless). Then you have a problem that the thing may not fit into the buffer tail. You must cut the buffer and wrap it leaving some empty space at the end of the buffer. Then when you overwrite only a part of an old thing in the buffer, you need to throw all that thing out. So yet another hole appears. Normally you would work with two indices: First_In and First_Out rotating them at the buffer length modulo. Keep in mind that First_In = First_Out is ambiguous meaning ether buffer full or buffer empty. You get the picture. The typical use case is writing stream representations of some objects into a ring buffer of stream elements array. A nice thing about modulo is that you can use very large indices, e.g. 64-bit. This allows you to order items in the buffer combining index and sequence number in a single unit and detecting if an item is not in the buffer anymore.

1 Like