12
\$\begingroup\$

A coworker, from a past programming life, called me up last week and asked about Trim Functions in COBOL. I sent her these PEFORMed "functions", which I've had lying about and without any professional opportunity to use them.

It's been my experience that most Cobologists working within their shop standard don't twiddle with CALLing real subroutines. Instead they PERFORM COPYed code, and refer to it as a subroutine. As such, I don't try to fight the tide.

Can anyone offer up any improvements? I know there are Intrinsic TRIM, TRIML, and TRIMR functions in some flavours of COBOL. However, I do not believe IBM Z-series Mainframe COBOL has these, so this is intended to be platform independent.

DATA DIVISION.

WORKING-STORAGE SECTION.

01  STR-VALUE-IN                 PIC X(1000).
01  STR-LENGTH-IN                PIC 9(05) BINARY.
01  STR-VALUE-OUT                PIC X(1000).
01  STR-LENGTH-OUT               PIC 9(05) BINARY.
01  STR-HANDLING-COUNTERS.
    05  STR-SUB01                PIC 9(05) BINARY.
    05  STR-TRAILING-SPACES      PIC 9(05) BINARY.
    05  STR-LEADING-SPACES       PIC 9(05) BINARY.
    05  STR-START                PIC 9(05) BINARY.
01  STR-HANDLING-SWITCHES.
    05  GRAPH-FOUND-SW           PIC X(01).
        88  GRAPH-FOUND                    VALUE '1'.
        88  GRAPH-NOT-FOUND                VALUE '2'.

PROCEDURE DIVISION.

    MOVE Whatever
      TO STR-VALUE-IN.
    MOVE FUNCTION LENGTH (Whatever)
      TO STR-LENGTH-IN.
    PERFORM 5200-RTRIM.
    MOVE STR-VALUE-OUT (1:STR-LENGTH-OUT)
      TO Wherever.

    MOVE Whatever
      TO STR-VALUE-IN.
    MOVE FUNCTION LENGTH (Whatever)
      TO STR-LENGTH-IN.
    PERFORM 5100-LTRIM.
    MOVE STR-VALUE-OUT (1:STR-LENGTH-OUT)
      TO Wherever.

    MOVE Whatever
      TO STR-VALUE-IN.
    MOVE FUNCTION LENGTH (Whatever)
      TO STR-LENGTH-IN.
    PERFORM 5300-TRIM.
    MOVE STR-VALUE-OUT (1:STR-LENGTH-OUT)
      TO Wherever.

5100-LTRIM.
* This resembles the VB function LTrim
* It removes any leading value less than or equal to space
* ASCII decimal 0 (Null) through ASCII decimal 32 (Space)
    PERFORM 5910-COMMON-TRIM-BEGIN.
    PERFORM 5930-TRIM-LEADING-SPACES.
    PERFORM 5990-COMMON-TRIM-END.

5200-RTRIM.
* This resembles the VB function RTrim
* It removes any trailing value less than or equal to space
* ASCII decimal 0 (Null) through ASCII decimal 32 (Space)
    PERFORM 5910-COMMON-TRIM-BEGIN.
    PERFORM 5950-TRIM-TRAILING-SPACES.
    PERFORM 5990-COMMON-TRIM-END.

5300-TRIM.
* This resembles the VB function Trim
* It removes any leading or trailing value less than or equal to space
* ASCII decimal 0 (Null) through ASCII decimal 32 (Space)
    PERFORM 5910-COMMON-TRIM-BEGIN.
    PERFORM 5930-TRIM-LEADING-SPACES.
    PERFORM 5950-TRIM-TRAILING-SPACES.
    PERFORM 5990-COMMON-TRIM-END.

5910-COMMON-TRIM-BEGIN.
    MOVE LOW-VALUES
      TO STR-HANDLING-COUNTERS.
    MOVE SPACES
      TO STR-HANDLING-SWITCHES.
    MOVE 1
      TO STR-START.
    MOVE STR-LENGTH-IN
      TO STR-LENGTH-OUT.

5930-TRIM-LEADING-SPACES.
    SET GRAPH-NOT-FOUND TO TRUE.
    PERFORM
      VARYING STR-SUB01 FROM 1 BY 1
        UNTIL STR-SUB01 > STR-LENGTH-IN
          OR GRAPH-FOUND
            IF (STR-VALUE-IN (STR-SUB01:1) > SPACES)
                MOVE STR-SUB01
                  TO STR-START
                SUBTRACT 1 FROM STR-SUB01
                    GIVING STR-LEADING-SPACES
                SUBTRACT STR-LEADING-SPACES
                    FROM STR-LENGTH-OUT
                SET GRAPH-FOUND TO TRUE
            END-IF
    END-PERFORM

5950-TRIM-TRAILING-SPACES.
    SET GRAPH-NOT-FOUND TO TRUE.
    PERFORM
      VARYING STR-SUB01 FROM STR-LENGTH-IN BY -1
        UNTIL STR-SUB01 < 1
          OR GRAPH-FOUND
            IF (STR-VALUE-IN (STR-SUB01:1) > SPACES)
                SUBTRACT STR-SUB01 FROM STR-LENGTH-IN
                    GIVING STR-TRAILING-SPACES
                SUBTRACT STR-TRAILING-SPACES
                    FROM STR-LENGTH-OUT
                SET GRAPH-FOUND TO TRUE
            END-IF
    END-PERFORM

5990-COMMON-TRIM-END.
    MOVE STR-VALUE-IN (STR-START:STR-LENGTH-OUT)
      TO STR-VALUE-OUT.
\$\endgroup\$
  • \$\begingroup\$ I looked in, and noticed you got a couple of downvotes. I wondered why. Woops. It seems it was me, I clicked down instead of up. If you can edit the question (maybe put in the missing full-stops/periods Edward referred to) I should be able to correct :-) At the moment the system refuses to let me, since it is more than four hours since I did it... Sorry about that. \$\endgroup\$ – Bill Woodger Dec 11 '14 at 0:19
  • \$\begingroup\$ Please do not edit the code after receiving answers. Rolled back Rev 3 → 2. \$\endgroup\$ – 200_success Dec 11 '14 at 6:39
  • 1
    \$\begingroup\$ @200_success Sorry, my fault. See comments above. \$\endgroup\$ – Bill Woodger Dec 11 '14 at 7:24
5
\$\begingroup\$

I'm not sure you can generalise the same routine to EBCDIC on the Mainframe. You'd want two different routines, one for data with sourced from the Mainframe, which would only trim spaces, and another with the source from an ASCII machine which would want to trim more, potentially.

We're picky about invalid data. The worst thing you can do with invalid data is ignore it (you miss the chance to find out why it is invalid).

What to trim? Why everything from space downwards? Why not just space and tab?

Leaving those points aside, since you specifically want what you want:

No need for 9(5) if your "string" is a maximum of 1000.

In COBOL the right-trim is questionable, since the MOVE STR-VALUE-OUT (1:STR-LENGTH-OUT) TO Wherever. is going to space-fill Wherever. The length of the actual data is important if you want to be able to use the "trimmed" value, other than for ignoring low-non-blank values. Unless Wherever is a variable-length field.

If you are using SET to change the values of the field with the 88-levels on, you shouldn't also MOVE SPACE to that field. A better definition is:

01  FILLER.
    05  FILER                      PIC X(01).
        88  GRAPH-FOUND            VALUE '1'.
        88  GRAPH-NOT-FOUND        VALUE '2'.

Now only the 88-levels, via SET, can amend the value. If you give the group or the field itself a name, someone will come along and use reference-modification. Like this, the field can only be reference by SET, which makes locating where its value changes much simpler.

There is no need to initialise it to space anyway, as you SET GRAPH-NOT-FOUND ... before using the field.

The MOVE LOW-VALUES will set all the subordinate binary fields to zero, but with this there is some danger that someone adds to the group and can't work out where their value gets clobbered. Yes, the group has got a name, but it happens. MOVE ZERO to the individual items.

In fact, looking at the code, there is no need to set any of those four fields to initial values, as they are always first referenced as the target of a verb, so their value prior to that is irrelevant.

You have the opportunity to simplify the inline PERFORMs.

IF ( STR-VALUE-IN 
      GREATER THAN SPACE )
*> PERFORM Exclude the leading/trailing <= space
ELSE
*> PERFORM There is no data present
END-IF

As well as saving you looking at 1000 bytes per Left/Right Trim, it simplifies...

PERFORM
  VARYING STR-SUB01 FROM 1 BY 1
    UNTIL ( STR-VALUE-IN ( STR-SUB01 : 1 ) 
             GREATER THAN SPACE )
END-PERFORM

The PERFORM is just used to find the first character, you know there is at least one because of the test above.

MOVE STR-SUB01
  TO STR-START
SUBTRACT 1 FROM STR-SUB01
  GIVING STR-LEADING-SPACES
SUBTRACT STR-LEADING-SPACES
  FROM STR-LENGTH-OUT

Similar for the trailing.

Note that the 88s discussed earlier have now disappeared, not needed.

You don't need STR-VALUE-OUT. You are using reference-modification, you can just use that on STR-VALUE-IN.

Since you are not concerning yourself with COBOL prior to the '85 Standard (although there are '74 Standard compiler still in use today) I'd suggest minimal use of full-stop/period in the PROCEDURE DIVISION.

End of PROCEDURE DIVISION header; end of pargraph/SECTION name; a full-stop/period on a line of its own as the last statement in a paragraph/SECTION; a full-stop/period on a line of its own as the last statement the program. (there is overlap here, since you don't have to have any paragraphs/SECTIONs in a program).

Prior to reference-modification ('85 Standard) mostly people would do a byte-by-byte MOVE.

The alternative to that, on an IBM Mainframe at least, is to use variable-length fields, but the byte-by-byte is the only portable solution across compilers from 1960 onwards. For '85 onwards, reference-modification is a portable solution.

From at least 1964, on an IBM Mainframe, you could use variable-length fields. This may have worked from day one of IBM COBOL, but 1964 is the earliest documentation I can find. The code to do this would have changed when IBM dropped its Compiler Extension allowing OCCURS DEPENDING ON with REDEFINES.

Being portable, even in a portable language like COBOL, isn't always easy.

I'd say that the code you produced, ignoring the EBCDIC point I made earlier, would work on every COBOL compiler to the '85 Standard or later, so it is pretty portable :-) (There are still some ICL, UNISYS and Tandem/HP machines actively using COBOL '74).

To generalise your routine further, you'd have to be able to indicate what values need to be trimmed. Which would make your inline-PERFORMs more complicated again, which is why I stuck to your existing spec :-)

\$\endgroup\$
5
\$\begingroup\$

It looks pretty good to me. Other than the missing periods at the end of 5930-TRIM-LEADING-SPACES and 5950-TRIM-TRAILING-SPACES, it works fine.

I do have a few minor suggestions:

  • The name STR-SUB01 is vague; STR-POS would be informative.

  • If the code is not going to be called very often, you could give STR-START a VALUE of 1 and shorten 5910-COMMON-TRIM-BEGIN to the more high-level

    5910-COMMON-TRIM-BEGIN.
        INITIALIZE STR-HANDLING-SWITCHES STR-HANDLING-COUNTERS
          ALL TO VALUE.
        MOVE STR-LENGTH-IN
          TO STR-LENGTH-OUT.
    
  • The code in the PERFORM loops is nested more deeply than the conditions, which makes it hard to tell where the condition ends and where the code begins.

To nitpick:

  • The reference to VB functions is only useful if the programmer reading it knows VB.
  • If your code is compiled on an EBCDIC system, then it will trim values less than decimal 64, making the comment on ASCII values irrelevant.

It's been my experience that most Cobologists working within their shop standard don't twiddle with CALLing real subroutines. Instead they PERFORM COPYed code, and refer to it as a subroutine. As such, I don't try to fight the tide.

But you should fight the tide! While PERFORMs modularise code, only CALLs modularise data as well. CALL allows you to control the data that goes in and out and to break monolithic programs into comprehensible chunks. Using COBOL is not an excuse for ignoring modern programming guidelines.

The above rant brings me to one final suggestion: the code would be even better if it was a subprogram.

\$\endgroup\$
  • \$\begingroup\$ Excellent insights, especially about EBCDIC. Until very recently, I'd only ever worked on Unisys 2200, Unisys MCP, and HP NonStop big boxes. No argument about making it a real subprogram with input parameters being the correct answer. \$\endgroup\$ – Kennah Nov 10 '14 at 1:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.