COBOL考古(七)

表格处理

本节介绍了表格的概念,表格是具有相同描述的数据项集合。下属的项目称为表格元素。表格是 COBOL 中数组的等价物。

本章的目标是为读者提供足够的信息,以便能够在 COBOL 程序中处理表格。

定义表格

要编写一个表格,我们需要给表格一个组名,并定义一个我们重复 n 次的下属项目。

1
2
3
4
01  TABLE-NAME.
05 SUBORDINATE-NAME OCCURS n TIMES.
10 ELEMENT1 PIC X(2).
10 ELEMENT2 PIC 9(2).

在上面的示例中,表格名是组项目的名称。表格还包含一个称为下属名的下属项目,我们将其重复 n 次。每个下属项目都有 2 个基本项目,即元素1和元素2。在这种情况下,我们将下属名称为表格元素的定义(因为它包含了OCCURS子句)。请注意,OCCURS子句不能在level-01描述中使用。

或者,我们也可以创建更简单的表格:

1
2
01  TABLE-NAME.
05 SUBORDINATE OCCURS n TIMES PIC X(10).

在这种情况下,表格名包含 n 个下属项目,每个下属项目最多可以包含 10 个字母数字字符。

我们还可以嵌套多个OCCURS元素,以创建具有附加维度的表格,最多可以有七个维度。请注意下面的示例:

1
2
3
4
5
6
7
8
01  PROGRAM-DETAILS.
05 PROGRAM-DEGREE PIC X(32).
05 COURSE-DETAILS OCCURS 10 TIMES.
10 COURSE-NAME PIC X(32).
10 INSTRUCTOR-ID PIC 9(10).
10 ASSIGNMENT-DETAILS OCCURS 8 TIMES.
15 ASSIGNMENT-NAME PIC X(32).
15 ASSIGMMENT-WEIGHTAGE PIC 9(03).

这里,我们正在定义一个包含 10 门课程的学位课程,每门课程将包含 8 项作业。如果我们不知道表格元素会发生多少次怎么办?为了解决这个问题,我们可以使用可变长度表格,使用“OCCURS DEPENDING ON(ODO)”子句,我们将在后面的部分详细讨论。

引用表格中的项目

虽然表格元素具有一个集体名称,但其中的各个项目没有唯一的名称。要引用项目,我们可以使用下标、索引或两者结合使用。

下标引用

下标引用是使用表格元素的数据名称,以及它的发生号码(称为下标)。最小的下标号码是 1,它定义了表格元素的第一个发生。我们还可以使用文字或数据名称作为下标。请注意,如果您使用的是数据名称,它必须是一个基本的数值整数。

1
2
3
4
01  TABLE-NAME.
05 TABLE-ELEMENT OCCURS 3 TIMES PIC X(03) VALUE "ABC".
...
MOVE "DEF" TO TABLE-ELEMENT (2)

在上面的示例中,第二个 TABLE-ELEMENT 将包含 “DEF” 而不是 “ABC”。

索引

或者,我们可以使用 OCCURS 子句的 INDEXED BY 短语创建一个索引。这个索引被添加到表的地址上,用于定位一个项目(作为从表的开头的偏移量)。例如,

1
05  TABLE-ELEMENT OCCURS 10 TIMES INDEXED BY INX-A   PIC X(03).

这里,INX-A 是一个索引名称。编译器将计算索引中的值,方法是将出现号减去 1,然后乘以表元素的长度。所以,例如,对于 TABLE-ELEMENT 的第二个出现,INX-A 中包含的二进制值是 (2-1) * 3,即 3。

如果您恰好有另一个具有相同数量的相同长度的表,您可以使用一个索引名称来引用这两个表。

我们还可以使用 USAGE IS INDEX 子句来定义索引数据项。这些索引数据项可以与任何表一起使用。例如,

1
2
3
4
5
6
7
8
77  INX-B  USAGE IS INDEX.
...
SET INX-A TO 10.
SET INX-B TO INX-A.
PERFORM VARYING INX-A FROM 1 BY 1 UNTIL INX-A > INX-B
DISPLAY TABLE-ELEMENT (INX-A)
...
END-PERFORM.

索引名称 INX-A 用于遍历 TABLE-ELEMENT 表,而 INX-B 用于保存表的最后一个元素的索引。通过这样做,我们最小化了偏移量的计算,并且不需要为 UNTIL 条件进行转换。

我们还可以通过一个基本整数数据项来递增或递减索引名称。例如,

1
SET INX-A DOWN BY 3

那里的整数代表出现次数。因此,在添加或减去索引之前,它将首先转换为索引值。

由于我们正在比较物理位移,因此不能使用索引数据项作为下标或索引。我们只能直接在 SEARCH 和 SET 语句中使用它,或者与索引进行比较。

以下示例显示了如何计算引用索引的元素的位移。

考虑以下二维表 TABLE-2D:

1
2
3
01  TABLE-2D.
05 TABLE-ROW OCCURS 2 TIMES INDEXED BY INX-A.
10 TABLE-COL OCCURS 5 TIMES INDEXED BY INX-B PIC X(4).

假设我们编写了以下索引:

1
TABLE-COL (INX-A + 2, INXB - 1)

这将导致计算到 TABLE-COL 元素的位移:

1
(INX-A 的内容) + (20 * 2) + (INX-B 的内容) - (4 * 1)

计算是基于元素的长度进行的。TABLE-ROW 的每个出现都有 20 个字节的长度(5 * 4),而 TABLE-COL 的每个出现都有 4 个字节的长度。

装载表格数据

有多种方法可以装载表格数据。第一种方法涉及从屏幕、文件或数据库动态装载表格。我们还可以在硬编码字段值上使用 REDEFINES 子句以及 OCCURS 子句。第三种方法是使用 INITIALIZE 语句,最后,我们还可以在定义表格时使用 VALUE 子句。

动态装载表格

要动态装载表格,我们需要使用 PERFORM 语句以及下标或索引。在执行此操作时,我们需要确保数据不超出分配给表格的空间。我们将在后面的章节中讨论文件处理和 PERFORM 子句的使用。例如,

1
2
3
4
5
6
7
PROCEDURE DIVISION.
...
PERFORM READ-FILE.
PERFORM VARYING SUB FROM 1 BY 1 UNTIL END-OF-FILE
MOVE DATA TO WS-DATA(SUB)
PERFORM READ-FILE
END-PERFORM.

在上面的示例中,我们执行一个读取文件的段落,然后我们将迭代文件的每一行直到结束,并将每个值放入表格中。

使用 REDEFINES 重新定义硬编码值

考虑以下示例,

1
2
3
4
5
6
7
8
9
10
WORKING-STORAGE SECTION.
01 NUMBER-VALUES.
05 FILLER PIC X(05) VALUE "One "
05 FILLER PIC X(05) VALUE "Two "
05 FILLER PIC X(05) VALUE "Three"
05 FILLER PIC X(05) VALUE "Four "
05 FILLER PIC X(05) VALUE "Five "

01 NUMBER-TABLES REDEFINES NUMBER-VALUES.
05 WS-NUMBER PIC X(05) OCCURS 5 TIMES.

在这里,我们将从1到5的拼写数字的硬编码值加载到一个表格中,通过使用 REDEFINES 子句实现。

使用 INITIALIZE 语句初始化表格

我们也可以使用 INITIALIZE 语句将数据加载到表格中。该表格将作为一个组项目进行处理,其中每个元素数据项将被识别和处理。例如,假设我们有以下表格:

1
2
3
4
01  TABLE-ONE.
05 TABLE-ELEMENT OCCURS 10 TIMES.
10 NUMBER-CODE PIC 9(02) VALUE 10.
10 ITEM-ID PIC X(02) VALUE "R3".

在这里,我们有一个包含10个元素的表格,每个元素都有自己的 NUMBER-CODE(值为10)和 ITEM-ID(值为 “R3”)。

我们可以将值3移动到表格中每个元素的数值数据项中,将值 “X” 移动到表格中每个元素的字母数字数据项中:

1
2
INITIALIZE TABLE-ONE REPLACING NUMERIC DATA BY 3.
INITIALIZE TABLE-ONE REPLACING ALPHANUMERIC DATA BY "X".

运行这两个 INITIALIZE 语句后,NUMBER-CODE 将包含值3,而 ITEM-ID 将包含值 “X “。

使用 VALUE 子句分配值

如果一个表格预期包含稳定的值,我们可以在定义表格时设置这些值。以前面章节中的 WEEK-DAY-TABLES 和 TABLE-ONE 为例,它们在定义时都分配了值。以下是一些更多的示例:

1
2
01  TABLE-TWO                            VALUE "1234".
05 TABLE-TWO-DATA OCCURS 4 TIMES PIC X.

在上面的示例中,表格中的字母数字组数据项 TABLE-TWO 使用了 VALUE 子句,用于初始化 TABLE-TWO-DATA 的四个元素。因此,在初始化之后,TABLE-TWO-DATA(1) 将包含字母数字 ‘1’,TABLE-TWO-DATA(2) 将包含字母数字 ‘2’,依此类推。

变长表格

如果在运行时之前无法确定表格元素会出现多少次,我们可以使用 OCCURS DEPENDING ON(ODO)子句定义一个变长表格。

1
X OCCURS 1 TO 10 TIMES DEPENDING ON Y

在上面的示例中,X 是 ODO 主体,Y 是 ODO 对象。

有一些因素会影响成功操作变长记录:

  • 正确计算记录长度

在这里,变量部分的长度是 DEPENDING ON 子句的对象和 OCCURS 子句的主体长度的乘积。

  • ODO 子句的对象数据是否符合其 PICTURE 子句的规范

我们必须确保 ODO 对象正确指定了表格元素的出现次数,否则程序可能会异常终止。

以下示例显示了如何使用 OCCURS DEPENDING ON 子句:

1
2
3
4
5
6
7
8
WORKING-STORAGE SECTION.
01 MAIN-AREA.
03 REC-1.
05 FIELD-1 PIC 9.
05 FIELD-2 OCCURS 1 TO 5 TIMES
DEPENDING ON FIELD-1 PIC X(05).
01 REC-2.
03 REC-2-DATA PIC X(50).

如果我们将 REC-1 移动到 REC-2,REC-1 的长度将在事先使用 FIELD-1 的当前值确定。如果 FIELD-1 不符合其 PICTURE 子句,结果是不可预测的。因此,我们需要确保 ODO 对象(FIELD-1)在将 REC-1 移动到 REC-2 之前具有正确的值。

另一方面,如果我们将 REC-2 移动到 REC-1,长度将使用最大出现次数来确定。但是,如果 REC-1 后跟一个可变位置的组,ODO 对象将用于计算 REC-1 的实际长度。以下提供了这种情况的示例:

1
2
3
4
5
6
7
8
9
01  MAIN-AREA.
03 REC-1.
05 FIELD-1 PIC 9.
05 FIELD-3 PIC 9.
05 FIELD-2 OCCURS 1 TO 5 TIMES
DEPENDING ON FIELD-1 PIC X(05).
03 REC-2.
05 FIELD-4 OCCURS 1 TO 5 TIMES
DEPENDING ON FIELD-3 PIC X(05).

因此,在上面的情况下,必须在将组项用作接收字段之前设置ODO对象的值。

搜索表格

有两种搜索表格的技术:串行搜索和二进制搜索。

二进制搜索可能比串行搜索更高效,但它要求表格项已经排序。

串行搜索

我们可以使用 SEARCH 语句进行串行搜索。搜索将从当前索引设置开始,并将一直进行,直到 WHEN 子句中的条件得到满足。要修改索引设置,我们可以使用 SET 语句。如果 WHEN 子句中有多个条件,搜索将在满足其中一个条件并保持索引指向满足条件的元素时结束。

例如,假设我们有一个名字列表:

1
2
3
4
5
6
7
8
9
10
11
12
77  PEOPLE-SEARCH-DATA                PIC X(20).
01 PEOPLE-SERIAL.
05 PEOPLE-NAME OCCURS 50 TIMES
INDEXED BY PL-IDX PIC X(20).
...
PROCEDURE-DIVISION.
...
SET PL-IDX TO 1.
SEARCH PEOPLE-NAME VARYING PL-IDX
AT END DISPLAY "Not found"
WHEN PEOPLE-SEARCH-DATA = PEOPLE-NAME(PL-IDX)
DISPLAY "Found".

上面的代码将从索引为1的位置开始搜索名字列表。如果找到 PEOPLE-SEARCH-DATA 的内容,它将显示 “Found”,否则将显示 “Not found”。

对于更复杂的用例,我们还可以使用嵌套的 SEARCH 语句。需要使用 END-SEARCH 来限定每个嵌套的 SEARCH 语句。

二进制搜索

要执行二进制搜索,我们可以使用 SEARCH ALL 语句。我们不需要设置索引,而是使用与 OCCURS 子句相关联的索引。要使用 SEARCH ALL 语句,表格必须指定 OCCURS 子句的 ASCENDING 或 DESCENDING KEY 子句,或两者都指定,并且它必须按指定的关键字排序。

使用 WHEN 子句,您可以测试在 ASCENDING 或 DESCENDING KEY 子句中命名的任何关键字。测试必须是等于条件,并且 WHEN 子句必须指定一个关键字或与关键字相关联的条件名称。

例如,假设我们有一个按升序排序的名字列表:

1
2
3
4
5
6
7
8
9
10
11
12
77  PEOPLE-SEARCH-DATA                PIC X(20).
01 PEOPLE-TABLE-BINARY.
05 PEOPLE-NAME OCCURS 50 TIMES
ASCENDING KEY IS PEOPLE-NAME
INDEXED BY PL-IDX PIC X(20).
...
PROCEDURE-DIVISION.
...
SEARCH ALL PEOPLE-NAME
AT END DISPLAY "Not found"
WHEN PEOPLE-SEARCH-DATA = PEOPLE-NAME(PL-IDX)
DISPLAY "Found".

上面的代码将搜索按字母顺序排序的名字列表。如果找到 PEOPLE-SEARCH-DATA 的内容,它将显示 “Found”,否则将显示 “Not found”。

实验

  1. 查看位于’id’.CBL数据集中的SRCHSER COBOL源代码成员。

  2. 从id.JCL中提交JCL成员SRCHSERJ,其中id是您的ID。这是编译并成功执行SRCHSER程序的地方。

  3. 查看SRCHSERJ作业输出,包括编译和执行的信息。

  4. 接下来,查看id.CBL数据集中的SRCHBIN COBOL源代码成员。

  5. 查看并从id.JCL下拉菜单中提交JCL成员SRCHBINJ。这是编译并执行SRCHBIN程序的地方。

  6. 查看SRCHBINJ作业输出,包括编译和执行的信息。

  7. 将SRCHSER与SRCHBIN进行比较。您是否注意到了以下差异?

    a. 观察表格的定义。

    b. 观察表格是如何从id.DATA数据集加载的。

    c. 观察SEARCH和SEARCH ALL语句。