1

I'm having a report which is called many times but I want the order to be different each time.

How can I change the "order by" depending on a variable.

For example

report print_label()

   If is_reprint
   then
       order by rpt.item_code, rpt.description
   else
       order by rpt.description, rpt.item_code
   end if

I tried passing in a variable when calling the report:

let scratch = "rpt.item_code, rpt.description"
start print_label(scratch)

And in the report I did:

order by scratch

But it didn't work...... Any other suggestions ?? Thank you!

Moses Davidowitz
  • 982
  • 11
  • 28

4 Answers4

2

The technique I've used for that type of problem is along the likes of

REPORT report_name(x)
DEFINE x RECORD
    param1,param2, ..., paramN ...,
    sort_method ...,
    data ...
END RECORD

ORDER [EXTERNAL] BY x.param1, x.param2, ..., x.paramN

BEFORE GROUP OF x.param1
    CASE 
        WHEN x.sort_method ...
            PRINT ...
        WHEN x.sort_method ...
            PRINT ...
    END CASE

BEFORE GROUP OF x.param2
    # similar technique as above

...

BEFORE GROUP OF x.paramN
    # similar technique as above

ON EVERY ROW
    PRINT ...

AFTER GROUP OF x.paramN
   # similar technique as above
...

AFTER GROUP OF x.param2
    # similar technique as above

AFTER GROUP OF x.param1
    # similar technique as above

... and then in the 4gl that calls the REPORT, populate x.param1, x.param2, ..., x.paramN with the desired parameters used for sorting e.g.

CASE x.sort_method 
    WHEN "product,branch"
        LET x.param1 = x.data.product_code
        LET x.param2 = x.data.branch_code
    WHEN "branch,product"
        LET x.param1 = x.data.branch_code
        LET x.param2 = x.data.product_code
END CASE
OUTPUT TO REPORT report_name(x.*)

So as per my example, that's a technique I've seen and used for things like stock reports. The warehouse/branch/store manager wants to see things ordered by warehouse/branch/store, and then by product/sku/item, whilst a product manager wants to see things ordered by product/sku/item, and then warehouse/branch/store. More analytical reports with more potential parameters can be done using the same technique. I think the record I have seen is 6. So in that case, much better with 1 report covering all 6!=720 potential combinations, rather than writing a separate report for each possible order combination.

So probably similar to Jonathan option 1, although I don't have the same reservations about the complexity. I don't recall catching at code review any of my junior developers getting it badly wrong. In fact if the report is generic enough, you'll find that you don't need to touch it too often.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
fourjs.reuben
  • 286
  • 3
  • 3
1

Short answer

The ORDER BY clause in an I4GL REPORT function has crucial effects on how the code implementing the report is generated. It is simply not feasible to rewire the generated code like that at run-time.

Therefore, you can't achieve your desired result directly.

Notes

Note that you should probably be using ORDER EXTERNAL BY rather than ORDER BY — the difference is that with EXTERNAL, the report can assume the data is presented in the correct order, but without, the report has to save up all the data (in a temporary table in the database), then select the data from the table in the required sorted order, making into into a two-pass report.

If you're brave and have the I4GL c-code compiler, you should take a look at the code generated for a report, but be aware it is some of the scariest code you're likely to encounter in a long time. It uses all sorts of tricks that you wouldn't dream of using yourself.

Workaround solutions — in outline

OK; so you can do it directly. What are your options? In my view, you have two options:

  1. Use two parameters specifically for choosing the ordering, and then use an ORDER BY (without EXTERNAL) clause that always lists them in a fixed order. However, when it comes time to use the report, choose which sequence you want the arguments in.

  2. Write two reports that differ only in the report name and the ORDER EXTERNAL BY clause. Arrange to call the correct report depending on which order you want.

Of these, option 2 is by far the simpler — except for the maintenance issue. Most likely, you'd arrange to generate the code from a single copy. That is, you'd save REPORT print_label_code_desc in one file, and then arrange to edit that into REPORT print_label_desc_code (use sed, for example) — and the edit would reverse the order of the names in the ORDER BY clause. This isn't all that hard to do in a makefile, though it requires some care.

Option 1 in practice

What does option 1 look like in practice?

DECLARE c CURSOR FOR
    SELECT * FROM SomeTable

START REPORT print_label -- optional specification of destination, etc.

FOREACH c INTO rpt.*
    IF do_item_desc THEN
        OUTPUT TO REPORT print_label(rpt.item_code, rpt.description, rpt.*)
    ELSE
        OUTPUT TO REPORT print_label(rpt.description, rpt.item_code, rpt.*)
    END IF
END FOREACH

FINISH REPORT print_label

The report function itself might look like:

REPORT print_label(col1, col2, rpt)
    DEFINE col1 CHAR(40)
    DEFINE col2 CHAR(40)
    DEFINE rpt  RECORD LIKE SomeTable.*

    ORDER BY col1, col2

FORMAT

    FIRST PAGE HEADER
        …
    BEFORE GROUP OF col1
        …
    BEFORE GROUP OF col2
        …
    ON EVERY ROW
        …
    AFTER GROUP OF col1
        …
    AFTER GROUP OF col2
        …
    ON LAST ROW
        …
END REPORT

Apologies for any mistakes in the outline code; it is a while since I last wrote any I4GL code.

The key point is that the ordered by values are passed specially to the report and used solely for controlling its organization. You may need to be able to print different details in the BGO (shorthand for BEFORE GROUP OF; AGO for AFTER GROUP OF) sections for the two columns. That will typically be handled by (gasp) global variables — this is I4GL and they are the normal way of doing business. Actually, they should be module variables rather than global variables if the report driver code (the code which calls START REPORT, OUTPUT TO REPORT and FINISH REPORT) is in the same file as the report itself. You need this because in general the reporting at the group levels (in the BGO and AGO blocks) will need different titles or labels depending on whether you're sorting code before description or vice versa. Note that the meaning of the group aggregates change depending on the order in the ORDER BY clause.

Note that not every report necessarily lends itself to such reordering. Simply running the BGO and AGO blocks in a different order is not sufficient to make the report output look sensible. In that case, you will fall back onto option 2 — or option 2A, which is write two separate reports that don't pretend to be just a reordering of the ORDER BY clause because the formatting of the data needs to be different depending on the ORDER BY clause.

As you can see, this requires some care — quite a bit more care than the alternative (option 2). If you use dynamic SQL to create the SELECT statement, you can arrange to put the right ORDER BY clause into the string that is then prepared so that the cursor will fetch the data in the correct order — allowing you to use ORDER EXTERNAL BY after all.

Summary

If you're a newcomer to I4GL, go with option 2. If your team is not reasonably experienced in I4GL, go with option 2. I don't like it very much, but it is the way that can be handled easily and is readily understood by yourself, your current colleagues, and those still to come.

If you're reasonably comfortable with I4GL and your team is reasonably experienced with I4GL — and the report layout really lends itself to being reorganized dynamically — then consider option 1. It is trickier, but I've done worse things in I4GL in times past.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • Thank you Jonathan. I tried option 1 before and it does not work. I'm getting the following error: "An ORDER BY or GROUP item specified within a report must be one of the report parameters. See error number -4415.) – Moses Davidowitz Sep 07 '16 at 20:59
  • In that case, you'll need to use Option 2. I'm tolerably certain Option 1 can be made to work, but it is far from trivial and won't necessarily give you better results than Option 2 anyway. In my experience, Option 2 — or 2A — is likely to be the one needed to meet the business requirements for the data layouts of the report. – Jonathan Leffler Sep 07 '16 at 21:02
  • I guess that is my only options now, I tried to avoid writing another report because it's a big report and all future modifications will have to be made on both reports. – Moses Davidowitz Sep 07 '16 at 21:04
  • I don't like suggesting code duplication much, but sometimes it is just about unavoidable (and that happens more often in I4GL than some other languages). Do consider whether you can 'generate' the second report from the first (or both from one template). If you can, it will save effort and inconsistency later. Be careful to document carefully that there are dual reports and changes to one must be made to the other if you can't just generate the second from the first. Personally, I'd put a lot of effort into a code generator if the report is big and important and still has to be created twice. – Jonathan Leffler Sep 07 '16 at 21:07
  • Sorry Jonathan, it was my mistake. Options 1 is working perfect! Thank you.... I didn't realize that I have to change it all over in the report (like before and after group) Thanks as always!! – Moses Davidowitz Sep 07 '16 at 21:26
  • I'm having an issue: I'm sorting the report by "line_no" but for some reason when I have a list with over 9 lines, all items from line 10 and up will show up on the report after line 1.. Any idea why this is happening? – Moses Davidowitz Sep 08 '16 at 21:55
  • 1
    Integer versus string. With strings, "100" sorts before "29", but not with numbers. You'll need to think about what's best. You have an integer (item code) and a string (item description), IIRC, which means you can't make the both integers. That means you probably need to format the numbers right-justified (or zero-padded on the left) so that the strings compare correctly. For example, 0000000100 and 0000000020 sort correctly as strings and as numbers. Using blanks in place of leading zeros also works. (I hope I got my counting correct!). – Jonathan Leffler Sep 08 '16 at 21:58
  • That's was the issue....... but I can't define it always the same way because col1 is used as 2 diff types.. (I wish I can DEFINE a variable in an IF statement) – Moses Davidowitz Sep 08 '16 at 22:06
  • 1
    You should be able to control it because the `col1` and `col2` values are completely under your control. They do not have to be a direct copy of the underlying values from the record; you can map them differently for your different types. All that matters is that the alpha sort imposed by the report gets the data into the right order — by whatever devious trick is necessary. I know my example code showed passing the columns from the record verbatim (except that the integer would internally be converted to string), but that was me doing the minimum of more or less necessary work. – Jonathan Leffler Sep 09 '16 at 00:06
  • I'll explain better: The 2 columns I'm using now is `rpt.line_no` (smallint) and `rpt.item_code` (char-type) - I'm calling the report 2 times. 1'st time `label_report(rpt.line_no, rpt.item_code)` and 2nd time `label_reoprt(rpt.item_code, rpt.line_no)`. And in the report I'm having `col1` and `col2` where both variables can be used for both, smallint and char-type.. how do I define them? If I define `col1` as a smallint, it will give me `error number -1213` when calling the second time having `col1` as the item_code. I hope I explained it well. Please help me!! Thanks a lot!! – Moses Davidowitz Sep 09 '16 at 13:40
  • The following worked: `IF LENGTH(col) = 1 THEN LET col1= "0", col1 CLIPPED END IF` - Thank you again. – Moses Davidowitz Sep 09 '16 at 15:38
0

You can have a case statement within the order by clause as follows :

order by 
       case 
         when 1 = 1 then 
            rpt.item_code, rpt.description
         else
            rpt.description, rpt.item_code
       end
mihirp724
  • 129
  • 5
  • 1
    No, I'm sorry, but that is not germane to the ORDER BY clause in an I4GL report — which is what this question is asking about. The ORDER BY clause in an I4GL report is not a part of a SELECT statement, even — it is a specification of how the data will be presented to the report which controls how the code inside the report will be used and hence the C code that is generated for the I4GL c-code compiler (and the corresponding p-code for the I4GL p-code compiler). – Jonathan Leffler Sep 07 '16 at 20:09
0

You can use prepare:

let query_txt="select ... "
If is_reprint then
  let query_txt=query_txt clipped, " order by rpt.item_code, 
  rpt.description"
else
  let query_txt=query_txt clipped, " order by rpt.description, 
  rpt.item_code"
end if
prepare statement1 from query_txt
declare cursor_name cursor for statement1

And now start report, use foreach etc, etc...

P.S. You must define query_txt as char long enough for whole text.

Brunner
  • 1,945
  • 23
  • 26