有没有一种方式可以在Ada中格式化文本输出?

10

有没有一种方法可以格式化输出的字符串?我想要一个漂亮的视图来显示以下输出

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

如果我使用C语言编程,我会进行类似以下的操作:

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

在Ada 05中有可能实现这种格式化吗?

PS:请忽略姓名、电话号码、地址和出生日期。这些都是我在30秒内编造的...


嗯,我想到了一种可能性,可以创建一个“格式字符串”类型,以比printf更安全的方式执行此类操作。这可能是某人的一个不错的小项目... - T.E.D.
6个回答

9

可以实现,但机制有点繁琐,需要更多的语言表述。
我通常会编写单独的过程来处理复杂输出,例如日期,并将其与其余字符串处理结合使用以提高可读性。

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;

这将强制使用固定长度字段,这将可选地允许截断长字符串,否则将后续条目移动到下一行。Set_Col将设置下一次写入的行位置,如果当前写入位置已经超过了请求的行,则可能将其放置在下一行上。
我加入了字符串截断作为使用数组切片和Text_IO输出操作的机会,但我通常不喜欢默认截断,因为允许字符串超出请求的宽度或缩进到下一行往往会使格式错误更加明显。
因此,给定上面的代码,打印出类似于您的第一行的内容可能如下所示:
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);

Ada中的文本IO通常很繁琐,但一般有一个优点,就是能够相对清晰地表达你正在做什么。


对于 Integer_IO,请加上 +1。为了方便起见,Integer_Text_IO 可能已经被定义。 - trashgod

4
请注意,在现今的C++中,printf()函数即将被弃用,取而代之的是使用带有流格式化器的流。这种方法很方便,但非常不安全(至少在某些情况下)。现今,开发者被鼓励使用C++流(以及它们各种各样的操作符)。
在Ada中,你可以使用字符串连接运算符&来操作字符串,类似于C++流中使用的流插入运算符<<。在某些方面,Ada的方法更好,因为你可以嵌套连接表达式,而使用流插入表达式则无法实现。
问题在于,没有与C++格式化器(如setfill()hexsetw())相对应的便利工具。虽然自己编写这些工具并不难(除了hex),但目前还不存在这些工具。
例如,setw()/setfill()的等效工具可能是这样的:
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"

如果您真的想要一个printf()接口,那么当然可以从Ada中调用printf()。您需要考虑的是在Ada的大小字符串和C的以空字符结尾的字符串之间进行转换,但这就是Ada.Interfaces.C.Strings存在的意义。


嗯...我以前没有用过它,但是在我的else分支中,Ada.Strings.Fixed."*"(Width - Source'length, Fill_Char)看起来可以让你摆脱递归。 - T.E.D.
1
此外,Ada巧妙地使用运算符重载的一个很好的例子。 - trashgod
嗯,只是出于好奇?printf() 到底有什么危险的地方吗?我从来不知道 printf() 会有危险哈哈。 - Chang Hyun Park
我正在尝试决定将Set_Fill作为一个函数是否非常巧妙或邪恶™。 - Greg
1
@Heartinpiece - printf()根本不会对参数进行类型检查,所以如果有一个参数写错了,很容易使程序崩溃。只需尝试将整数传递给“%s”格式,并查看会发生什么。我经常自己这样做,并且自80年代中期以来一直在使用C语言。此外,这个函数族(特别是sprintf())可能是世界上最大的缓冲区溢出攻击目标。有整个黑客网站致力于帮助找到并利用sprintf()的用法。 - T.E.D.
显示剩余2条评论

4

针对这种特定格式设置,有辅助工具可用。

Ada.Text_IO.Integer_IO

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

将一个带有Item 的字段对齐到右侧,并使用空白字符填充。其中Width是字段宽度,Base默认为10。

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;

返回格式化的字符串。其中Count是字段宽度,Pad是填充字段的内容。Head将字符串左对齐。Tail将字符串右对齐。
请使用8个字符长的列宽,并使用破折号作为填充字符。
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, '-'));

输出

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

属性 discrete_type'Width

返回一个长度,该离散类型需要以文本形式表示。

示例

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;

输出

   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

我建议创建一个用于电话号码的类型,我不确定它应该是带有前导零的字符串还是数字,电话号码的长度可能不同。


我忘记了 'Width 属性,太好了。 - Álex

3
你也可以使用GNAT.Formatted_String包。它至少能在Ada 2012中使用(无法确定它是否存在于Ada 2005中)。它与printf的用法非常相似,但有轻微的语法差异。
以下是一个可工作的简单示例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;

输出:

(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 !

以下是关于IT技术的一个例子,这里提供了输入示例: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;

输出:

(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

最好的例子可以在提供给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.

在循环中使用此方法时有一个小陷阱:当解析格式(使用-(myformat & value))时,+"myformat: %s"似乎会被更改。诀窍是在每次循环开始时重新执行格式赋值。 - LoneWanderer
以下是同一源文件中的解释:-- 请注意,Formatted_String对象不能被重复使用,因为它作为最终结果的接收者。也就是说,每次使用“&”都会逐步构建最终结果字符串,可以通过下面的“-”例程检索。 最好提前警告。 - LoneWanderer
添加了使用 OP 数据的工作示例。 - LoneWanderer
嗨。你的例子在GNAT 13上不起作用。当我尝试使用它时,我得到了subtype mark required in this contextoperator "+" not defined for subtype of "Standard.String"的错误。我希望除了试图浏览源代码之外,还有其他类型的文档可用于这个库... - undefined
抱歉...神奇的是,如果我添加use GNAT.Formatted_String;(并相应修复变量声明),它就能工作。我原以为这只是一种语法糖...结果它的作用远不止于此 :| - undefined

3

是的,有办法。虽然它不像在c语言中那么容易。

请参考§A.4.4 有界长度字符串处理,了解如何创建预定义大小的字符串,并使用integer'image来转换您的数字。 &运算符可用于连接字符串,并使用ada.text_io.put_line()进行输出。


链接已更新。'Image' 属性会在正数前添加一个空格,而 Ada.Text_IO.Integer_IO 不会。请参见 §A.4.3 固定长度字符串处理 - trashgod

2
您可能会喜欢这个简单的 纸牌游戏模拟,它使用 Ada.Strings.Fixed 格式化 ASCII 图表的范围轴标签。请参阅 function Label,该函数使用 TailTrim 格式化 LowerUpper 值的 Integer'Image

代码:

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;

控制台:
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 |*

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接