10

Is there a way to format the string being outputted? I'm trying get a pretty view of the following output

1: Ashley | 01033438392 | Wellington, New Zealand | 1987- 4-14  
2: Aloha | 01087651234 | Hawaii, United States of America | 1988- 9-23
3: Jack | 01082840184 | Beijing, China | 1989- 6-19

If I was programming in C, I would do something like

printf("%10s | %11s | %20s | %4d-%2d-%2d\n",name,phone,address,year,month,day);

Would it be possible to do this kind of formatting in Ada 05?

PS Please just ignore the names, phone numbers, address, and birthdate. I made them up in like 30 seconds...

T.E.D.
  • 44,016
  • 10
  • 73
  • 134
Chang Hyun Park
  • 521
  • 1
  • 6
  • 22
  • Hmmm. It occurrs to me that it would be possible to create a "format string" type that could do stuff like this in a more type-safe manner than `printf`. That might be a nice little project for someone... – T.E.D. May 18 '11 at 13:40

6 Answers6

9

It can be done, but the mechanisms are a bit cumbersome and quite a bit more verbose.
What I would generally do is write separate procedures to handle your more complicated output, e.g. dates and use that with the rest of the string handling for clarity.

package Integer_IO is new Ada.Text_IO.Integer_IO (Integer);

procedure Output_Date ( Day : in Integer; Month: in Integer; Year: in Integer) is 
begin  
  Integer_IO.Put(Item => Day, Width => 2); 
  Text_IO.Put("-");
  Integer_IO.Put(Item => Month, Width => 2); 
  Text_IO.Put("-");
  Integer_IO.Put(Item => Year, Width => 4);
end Output_Date;

procedure Output_String ( Item : in String; 
                          Width : in Integer; 
                          Separator : in String := "|";
                          Truncate : Boolean := False) is 
  Field_Index : Integer := Text_IO.Col;
begin 
  if Item'length > Width and Truncate then 
    Text_IO.Put(Item(1..Width) & Separator);
  else 
    Text_IO.Put(Item) & Separator;
  end if;

  Text_IO.Set_Col ( Field_Index + Width + 1 );
end Output_String;

This will enforce fixed length fields, which will optionally allow truncation of long strings, or else move subsequent entries onto the next line. Set_Col will set the line position for the next write, potentially placing it on the next line if the current write position has already exceeded the one requested.

I threw string truncation in there as a chance to use array slicing and Text_IO's output manipulation, but I'm not generally a fan of default truncation, as allowing the string to overrun the requested width or indenting on the next line tend make formatting errors more obvious.

So printing out something like your first line, given the code above, might look something like:

Name  : String  := "Ashley"
Phone : String  := "01033438392"
Address: String := "Wellington, New Zealand"

Day    : Integer := 14;
Month : Integer  := 4;
Year   : Integer := 1987;

Output_String(Item=> Name,    Width => 10);
Output_String(Item=> Phone,   Width => 11);
Output_String(Item=> Address, Width => 20);
Output_Date(Day,Month,Year);

Text IO in Ada is usually cumbersome, but generally has the virtue of making what you are doing relatively clear.

T.E.D.
  • 44,016
  • 10
  • 73
  • 134
Greg
  • 1,480
  • 3
  • 15
  • 29
  • +1 for `Integer_IO`. For convenience, [`Integer_Text_IO`](http://www.adaic.org/resources/add_content/standards/05rm/html/RM-A-10-8.html) may already be defined. – trashgod May 17 '11 at 18:42
4

There is auxiliary tools for this particular format setup.

Package Ada.Text_IO.Integer_IO

procedure Put(Item : in Num; Width : in Field := Default_Width; Base : in Number_Base := Default_Base);

Puts a field with Item aligned to right and white-space character as filler. Where Width is field width and Base is 10 as defualt.

Package Ada.Strings.Fixed

function Head (Source : in String; Count : in Natural; Pad : in Character := Space) return String;
function Tail (Source : in String; Count : in Natural; Pad : in Character := Space) return String;

Returns a formatted string. Where Count is the field width and Pad is filler for the field. Head aligns string to left. Tail aligns string to right.

Let column width be 8 character long and use dash as filler.

Put_Line (Head ("Ashley", 8, '-'));
Put_Line (Head ("Aloha", 8, '-'));
Put_Line (Head ("Jack", 8, '-'));
Put_Line (Tail ("Ashley", 8, '-'));
Put_Line (Tail ("Aloha", 8, '-'));
Put_Line (Tail ("Jack", 8, '-'));

Output

Ashley--
Aloha---
Jack----
--Ashley
---Aloha
----Jack

Attribute discrete_type'Width

Returns a length which the discrete type requires to be represented as text.

Example

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Float_Text_IO; use Ada.Float_Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
with Ada.Strings.Fixed; use Ada.Strings.Fixed;
with Ada.Calendar; use Ada.Calendar;

procedure Test is

   subtype Index is Positive range 95 .. 1223;

   procedure Put_Line ( I : in out Index; Name : String; Phone : Natural; Address : String; T : in out Time ) is
   begin
      Put (I, Index'Width);
      Put (": ");
      Put (Head (Name, 10, ' '));
      Put (" | ");
      Put (Tail (Phone'Img (Phone'Img'First + 1 .. Phone'Img'Last), 13, '0'));
      Put (" | ");
      Put (Head (Address, 20, ' '));
      Put (Year (T), Year_Number'Width);
      Put ("-");
      Put (Month (T), Month_Number'Width);
      Put ("-");
      Put (Day (T), Day_Number'Width);
      I := Positive'Succ (I);
      T := T + Duration (60 * 60 * 24 * 3);
      New_Line;
   end;

   I : Index := Index'First;
   Now : Time := Clock;
begin

   Put_Line (I, "Ashley", 1033438392, "Wellington, New Zealand", Now);
   Put_Line (I, "Aloha", 01087651234, "Hawaii, United States of America", Now);
   Put_Line (I, "Jack", 01082840184, "Beijing, China", Now);
   I := Index'Last - 3;
   Put_Line (I,"Ashley", 1033438392, "Wellington, New Zealand", Now);
   Put_Line (I,"Aloha", 01087651234, "Hawaii, United States of America", Now);
   Put_Line (I,"Jack", 01082840184, "Beijing, China", Now);

end;

Output

   95: Ashley     | 0001033438392 | Wellington, New Zeal 2015-  5- 24
   96: Aloha      | 0001087651234 | Hawaii, United State 2015-  5- 27
   97: Jack       | 0001082840184 | Beijing, China       2015-  5- 30
 1220: Ashley     | 0001033438392 | Wellington, New Zeal 2015-  6-  2
 1221: Aloha      | 0001087651234 | Hawaii, United State 2015-  6-  5
 1222: Jack       | 0001082840184 | Beijing, China       2015-  6-  8

I would recommend to create a type for a phone number, I dunno if it should be string or number with the about of heading zeros, phone number can have different length I guess.

Jossi
  • 1,020
  • 1
  • 17
  • 28
4

Note that in C++ these days printf() is on the verge of being depreciated, in favor of using streams with stream formatters. It is convenient, but massively unsafe (in at least a couple senses of the word). These days developers are encouraged to use C++ streams (with their assorted manipulators) instead.

In Ada you can manipulate strings in a very similar style to C++ streams using the string catenation operator & where C++ folks use the stream insertion operator (<<). In some ways, Ada's method is better because you can nest catenated expressions, which you can't do with stream-inserted expressions.

The problem here is that there aren't any handy equivalents to the C++ formatters like setfill(), hex, and setw(). There really ought to be, and (hex excepted) they aren't tough to write yourself, but for now they don't exist.

For example, a setw()/setfill() equivalent would be something like:

Fill_Char : Character := ' ';

function Set_Fill (New_Fill : Character) return String is
begin
    Fill_Char := New_Fill;
    return "";
end Set_Fill;

--// Dumb tail-recursive implementation. 
function Set_Width(Source : in String; Width : in Positive) return String is
begin
    if Width <= Source'length then --'
        return Source;
    else 
        return Fill_Char & Set_Width(Source, Width - 1);
    end if;
end Set_Width;

Unfilled_String : constant String := "123456";
Filled_String : constant String := Set_Width(Unfilled_String & Set_Fill('0'), 8);
--// The above string should end up being "00123456"

If you really want a printf() interface, printf() is quite callable from Ada of course. You have to worry about transitioning between Ada's sized strings and C's nul-terminated strings, but that's what Ada.Interfaces.C.Strings is there for.

T.E.D.
  • 44,016
  • 10
  • 73
  • 134
  • Hmm.. I haven't used it before, but it looks like `Ada.Strings.Fixed."*"(Width - Source'length, Fill_Char)` in my `else` branch allow you to get rid of the recursion. – T.E.D. May 17 '11 at 20:40
  • 1
    Also, a good example of Ada's judicious use of operator overloading. – trashgod May 18 '11 at 00:15
  • @oenone - It is in fact operator overloading if you write it like this: `return ((Width - Source'length) * Fill_Char) & Source;`. – T.E.D. May 18 '11 at 13:03
  • Hmm Just out of curiosity? What exaclty is dangerous about printf()? I never knew that printf could be dangerous lol – Chang Hyun Park May 18 '11 at 15:58
  • I am trying to decide whether making Set_Fill a function is incredibly crafty or Evil™ – Greg May 18 '11 at 17:09
  • @Greg - I'd probably vote for Evil. This is what C++ does with `std::setfill()` though, which is what I was trying to duplicate. I'm not a big fan of adding implicit state, but its their paradigm. – T.E.D. May 18 '11 at 18:34
  • 1
    @Heartinpiece - `printf()` doesn't type check the arguments at all, so it is very easy to crash your program by getting one wrong. Just try feeding an integer to a "%s" format and see what happens. I do it myself regularly and I've been using C since the mid 80's. Additionally, this family of routines (particularly `sprintf()`) is probably the single greatest target of buffer overrun exploits in the world. There are entire hacker websites dedicated to helping find and exploit uses of `sprintf()`. – T.E.D. May 18 '11 at 19:03
3

Yes there is. Although its not as easy as in c.

Have a look at §A.4.4 Bounded-Length String Handling for how to create strings of a predefined size, and use integer'image to convert your numbers. The & operator is useful to concatenate strings and output using ada.text_io.put_line().

trashgod
  • 203,806
  • 29
  • 246
  • 1,045
NWS
  • 3,080
  • 1
  • 19
  • 34
  • Link updated. The 'Image attribute prepends a space for positive numbers, while `Ada.Text_IO.Integer_IO` does not. See also [§A.4.3 Fixed-Length String Handling](http://www.adaic.org/resources/add_content/standards/05rm/html/RM-A-4-3.html). – trashgod May 17 '11 at 18:40
2

You can also use the GNAT.Formatted_String package. It works at least with Ada 2012 (can't check if it exists in Ada 2005). It is very similar to printf usage, but with a slight syntactical difference.

Here is a working simple example http://tpcg.io/iJwfWa :

with Ada.Text_IO; use Ada.Text_IO;
with GNAT.Formatted_String; use GNAT.Formatted_String;

procedure Hello is
  formatStr : constant String := "Hello, %-5s !";
  nameArray : constant array (1..3) of String  (1..4) :=
  (1 => "_Foo",
   2 => "_Bar",
   3 => "_Fuu");
  gnatFormat : Formatted_String := +(formatStr); -- initialisation needed
begin
  for index in nameArray'range loop
    gnatFormat := +(formatStr); --reaffectation needed
    Put_Line(-(gnatFormat & nameArray(index)));
  end loop;
end Hello;

Output :

(GNATMAKE v7.1.1 on https://www.tutorialspoint.com/compile_ada_online.php)
$gnatmake -o hello *.adb
gcc -c hello.adb
gnatbind -x hello.ali
gnatlink hello.ali -o hello

$hello
Hello,  _Foo !
Hello,  _Bar !
Hello,  _Fuu !

Another example with your inputs http://tpcg.io/iJwfWa :

with Ada.Text_IO; use Ada.Text_IO;
with GNAT.Formatted_String; use GNAT.Formatted_String;
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;

procedure Hello is
  formatStr : constant String := "%-10s | %11d | %35s | %4d-%2d-%2d";
  type T_element_descriptor is record
    name : Unbounded_String;
    number : Integer;
    city : Unbounded_String;
    birth_y : Integer; -- should use a date object ...
    birth_m : Integer;
    birth_d : Integer;
  end record;
  elementArray : constant array (1..3) of T_element_descriptor :=
  (1 => (To_Unbounded_String("Ashley"), 01033438392, To_Unbounded_String("Wellington, New Zealand"), 1987, 4, 14),
   2 => (To_Unbounded_String("Aloha"), 01087651234, To_Unbounded_String("Hawaii, United States of America"), 1988, 9, 23),
   3 => (To_Unbounded_String("Jack"),  01082840184, To_Unbounded_String("Beijing, China"), 1989, 6, 19));
  gnatFormat : Formatted_String := +formatStr;
begin
  for index in elementArray'range loop
    gnatFormat := +(formatStr);
    Put_Line(-(gnatFormat
    & To_String(elementArray(index).name)
    & elementArray(index).number
    & To_String(elementArray(index).city)
    & elementArray(index).birth_y
    & elementArray(index).birth_m
    & elementArray(index).birth_d
    ));
  end loop;
end Hello;

Outputs:

(GNATMAKE v7.1.1 on https://www.tutorialspoint.com/compile_ada_online.php)
$gnatmake -o hello *.adb
gcc -c hello.adb
gnatbind -x hello.ali
gnatlink hello.ali -o hello

$hello
Ashley     |  1033438392 |             Wellington, New Zealand | 1987- 4-14
Aloha      |  1087651234 |    Hawaii, United States of America | 1988- 9-23
Jack       |  1082840184 |                      Beijing, China | 1989- 6-19

Best example is given in the g-forstr.ads file provided with gnat :

--  This package add support for formatted string as supported by C printf()

--  A simple usage is:
--
--     Put_Line (-(+"%s" & "a string"));
--
--  or with a constant for the format:
--
--     declare
--       Format : constant Formatted_String := +"%s";
--     begin
--       Put_Line (-(Format & "a string"));
--     end;
--
--  Finally a more complex example:
--
--     declare
--        F : Formatted_String := +"['%c' ; %10d]";
--        C : Character := 'v';
--        I : Integer := 98;
--     begin
--        F := F & C & I;
--        Put_Line (-F);
--     end;

--  Which will display:

--     ['v' ;         98]

--  Each format specifier is: %[flags][width][.precision][length]specifier

--  Specifiers:
--    d or i    Signed decimal integer
--    u         Unsigned decimal integer
--    o         Unsigned octal
--    x         Unsigned hexadecimal integer
--    X         Unsigned hexadecimal integer (uppercase)
--    f         Decimal floating point, lowercase
--    F         Decimal floating point, uppercase
--    e         Scientific notation (mantissa/exponent), lowercase
--    E         Scientific notation (mantissa/exponent), uppercase
--    g         Use the shortest representation: %e or %f
--    G         Use the shortest representation: %E or %F
--    c         Character
--    s         String of characters
--    p         Pointer address
--    %         A % followed by another % character will write a single %

--  Flags:

--    -         Left-justify within the given field width;
--              Right justification is the default.

--    +         Forces to preceed the result with a plus or minus sign (+ or -)
--              even for positive numbers. By default, only negative numbers
--              are preceded with a - sign.

--    (space)   If no sign is going to be written, a blank space is inserted
--              before the value.

--    #         Used with o, x or X specifiers the value is preceeded with
--              0, 0x or 0X respectively for values different than zero.
--              Used with a, A, e, E, f, F, g or G it forces the written
--              output to contain a decimal point even if no more digits
--              follow. By default, if no digits follow, no decimal point is
--              written.

--    ~         As above, but using Ada style based <base>#<number>#

--    0         Left-pads the number with zeroes (0) instead of spaces when
--              padding is specified.

--  Width:
--    number    Minimum number of characters to be printed. If the value to
--              be printed is shorter than this number, the result is padded
--              with blank spaces. The value is not truncated even if the
--              result is larger.

--    *         The width is not specified in the format string, but as an
--              additional integer value argument preceding the argument that
--              has to be formatted.
--  Precision:
--    number    For integer specifiers (d, i, o, u, x, X): precision specifies
--              the minimum number of digits to be written. If the value to be
--              written is shorter than this number, the result is padded with
--              leading zeros. The value is not truncated even if the result
--              is longer. A precision of 0 means that no character is written
--              for the value 0.

--              For e, E, f and F specifiers: this is the number of digits to
--              be printed after the decimal point (by default, this is 6).
--              For g and G specifiers: This is the maximum number of
--              significant digits to be printed.

--              For s: this is the maximum number of characters to be printed.
--              By default all characters are printed until the ending null
--              character is encountered.

--              If the period is specified without an explicit value for
--              precision, 0 is assumed.

--    .*        The precision is not specified in the format string, but as an
--              additional integer value argument preceding the argument that
--              has to be formatted.
LoneWanderer
  • 3,058
  • 1
  • 23
  • 41
  • There is a small pitfall while using this inside a loop : the `+"myformat: %s"` seems to be altered when resolving the formatting (with `-(myformat & value)` in some cases. The trick is to re-perform affectation of format at each loop start. – LoneWanderer Jun 18 '18 at 12:21
  • Here is the explanation (found in the same source file): `-- Note that a Formatted_String object can't be reused as it serves as recipient for the final result. That is, each use of "&" will build incrementally the final result string which can be retrieved with the "-" routine below.` Better be warned. – LoneWanderer Jun 18 '18 at 12:35
  • Working examples added with OP data. – LoneWanderer Jun 27 '18 at 11:50
2

You might like this simple card game simulation that uses Ada.Strings.Fixed to format the range axis labels for an ASCII graph. See function Label, which uses Tail and Trim to format the Integer'Image of a Lower and Upper value.

Code:

function Label (J : Integer) return String is
   use Ada.Strings; use Ada.Strings.Fixed;
   Lower : String := Integer'Image(J * Bin_Size);
   Upper : String := Integer'Image((J + 1) * Bin_Size);
begin
   return Tail(Trim(Lower, Left), 4, '0') & "-" &
      Tail(Trim(Upper, Left), 4, '0') & " |*";
end Label;

Console:

Distribution of lengths:
0000-0100 |**********
0100-0200 |*****************************
0200-0300 |**********************
0300-0400 |***************
0400-0500 |**********
0500-0600 |*******
0600-0700 |****
0700-0800 |****
0800-0900 |**
0900-1000 |**
1000-1100 |*
1100-1200 |*
trashgod
  • 203,806
  • 29
  • 246
  • 1,045