Control Break Concepts

 

Control break processing is a technique used to process a sorted sequential set of data (such as a text file, resultset of a database query, etc.) that contains groups of records with common values in one or more fields which are designated as the control fields. An example of a control field would be the sale date in a file of sales records for a particular month. Suppose you had eight sales on the first day of the month, and ten sales on the second day of the month. The set of eight records for day one and the set of ten records for day two each form a control group. So a control group is a set of records that all have the same value in the control field. As your program process the sales records, it notices that the date has changed between the eighth and ninth record. This change in the value of the control field is called a control break. When the control break is detected, the program will typically perform an action for the previous control group, such as print a subtotal for that group.

 

Control break processing typically comes into play when writing report programs "manually" (i.e., without the benefit of a report designer such as Crystal Reports, the ReportViewer, the MS-Access Report Designer, or others). Such report designers handle all of this control break stuff behind the scenes while you visually prepare the format of the report. Writing report programs manually (i.e., handling all details of the printing and subtotalling logic yourself within the program) is typically done in mainframe environments using a language like COBOL. So why bother with report program logic and control break logic in VB? Well, there are times when a plain-text printed report will get the job done and you can avoid the processing overhead of using a report designer. And even if you never have to write a report program "manually" (using native VB code), knowledge of control break processing can come in handy for other programming tasks, such as populating a TreeView control or generating an XML file.

 

In this article we will examine control break logic in the context of processing a sequential file of sales order data for the Northwind Traders company. (Note: This data was extracted from the Northwind sample database that Microsoft provides with VB and modified for the purpose of the examples presented here.) We will also examine some old-school report program logic techniques.

 

The Northwind sales file is a comma-delimited file consisting of the following fields:

·         Year-Month (YYYYMM format). For example, if the sale occurred in October 2004, the value would be "200410".

·         Order Date (The date on which the sale was made, M/D/YYYY format).

·         Order Number (5-digit order identifier)

·         Customer ID (5-character customer code identifier)

·         Product Description (textual description of the product sold, 32 characters max)

·         Unit Price (numeric unit price of the product sold)

·         Quantity (integer representing the quantity of the product sold)

·         Discount Percentage (Numeric decimal value representing the percent discount given to the customer on the item, if any. Could be 0, or 0.1 for 10%, 0.25 for 25%, etc.)

 

When the sample programs process this file, fields from each input record will be printed along with three calculated values:

·         Extended Price (the base amount of a sale line item, calculated as Unit Price * Quantity)

·         Discount Amount (the amount "off", calculated as Extended Price * Discount Percentage)

·         Sale Total (the total amount for a sale line item, calculated as Extended Price – Discount Amount)

 

The content of the input file (NWSALES.TXT) is shown below:

 

200410,10/3/2004,10295,VINET,Gnocchi di nonna Alice,30.4,4,0

200410,10/4/2004,10296,LILAS,Pavlova,13.9,30,0

200410,10/4/2004,10296,LILAS,Gudbrandsdalsost,28.8,15,0

200410,10/4/2004,10296,LILAS,Queso Cabrales,16.8,12,0

200410,10/5/2004,10297,BLONP,Mozzarella di Giovanni,27.8,20,0

200410,10/5/2004,10297,BLONP,Chartreuse verte,14.4,60,0

200410,10/6/2004,10298,HUNGO,Chang,15.2,40,0

200410,10/6/2004,10298,HUNGO,Inlagd Sill,15.2,40,0.25

200410,10/6/2004,10298,HUNGO,Raclette Courdavault,44,30,0.25

200410,10/6/2004,10298,HUNGO,Tarte au sucre,39.4,15,0

200410,10/7/2004,10299,RICAR,Teatime Chocolate Biscuits,7.3,15,0

200410,10/7/2004,10299,RICAR,Outback Lager,12,20,0

200410,10/10/2004,10300,MAGAA,Louisiana Hot Spiced Okra,13.6,30,0

200410,10/10/2004,10300,MAGAA,Scottish Longbreads,10,20,0

200410,10/10/2004,10301,WANDK,Boston Crab Meat,14.7,10,0

200410,10/10/2004,10301,WANDK,Gnocchi di nonna Alice,30.4,20,0

200410,10/11/2004,10302,SUPRD,Alice Mutton,31.2,40,0

200410,10/11/2004,10302,SUPRD,Rössle Sauerkraut,36.4,28,0

200410,10/11/2004,10302,SUPRD,Ipoh Coffee,36.8,12,0

200410,10/12/2004,10303,GODOS,Boston Crab Meat,14.7,40,0.1

200410,10/12/2004,10303,GODOS,Louisiana Fiery Hot Pepper Sauce,16.8,30,0.1

200410,10/12/2004,10303,GODOS,Scottish Longbreads,10,15,0.1

200410,10/13/2004,10304,TORTU,Maxilaku,16,30,0

200410,10/13/2004,10304,TORTU,Raclette Courdavault,44,10,0

200410,10/13/2004,10304,TORTU,Fløtemysost,17.2,2,0

200410,10/14/2004,10305,OLDWO,Carnarvon Tigers,50,25,0.1

200410,10/14/2004,10305,OLDWO,Thüringer Rostbratwurst,99,25,0.1

200410,10/14/2004,10305,OLDWO,Chartreuse verte,14.4,30,0.1

200410,10/17/2004,10306,ROMEY,Nord-Ost Matjeshering,20.7,10,0

200410,10/17/2004,10306,ROMEY,Perth Pasties,26.2,10,0

200410,10/17/2004,10306,ROMEY,Tourtière,5.9,5,0

200410,10/18/2004,10307,LONEP,Scottish Longbreads,10,3,0

200410,10/18/2004,10307,LONEP,Tarte au sucre,39.4,10,0

200410,10/19/2004,10308,ANATR,Outback Lager,12,5,0

200410,10/19/2004,10308,ANATR,Gudbrandsdalsost,28.8,1,0

200410,10/20/2004,10309,HUNGO,Chef Anton's Cajun Seasoning,17.6,20,0

200410,10/20/2004,10309,HUNGO,Grandma's Boysenberry Spread,20,30,0

200410,10/20/2004,10309,HUNGO,Singaporean Hokkien Fried Mee,11.2,2,0

200410,10/20/2004,10309,HUNGO,Ipoh Coffee,36.8,20,0

200410,10/20/2004,10309,HUNGO,Fløtemysost,17.2,3,0

200410,10/21/2004,10310,THEBI,Pavlova,13.9,10,0

200410,10/21/2004,10310,THEBI,Tarte au sucre,39.4,5,0

200410,10/21/2004,10311,DUMON,Gudbrandsdalsost,28.8,7,0

200410,10/21/2004,10311,DUMON,Singaporean Hokkien Fried Mee,11.2,6,0

200410,10/24/2004,10312,WANDK,Rhönbräu Klosterbier,6.2,10,0

200410,10/24/2004,10312,WANDK,Perth Pasties,26.2,20,0

200410,10/24/2004,10312,WANDK,Rössle Sauerkraut,36.4,4,0

200410,10/24/2004,10312,WANDK,Ipoh Coffee,36.8,24,0

200410,10/25/2004,10313,QUICK,Inlagd Sill,15.2,12,0

200410,10/26/2004,10314,RATTC,Mascarpone Fabioli,25.6,40,0.1

200410,10/26/2004,10314,RATTC,Escargots de Bourgogne,10.6,30,0.1

200410,10/26/2004,10314,RATTC,Tarte au sucre,39.4,25,0.1

200410,10/27/2004,10315,ISLAT,Outback Lager,12,30,0

200410,10/27/2004,10315,ISLAT,Sasquatch Ale,11.2,14,0

200410,10/28/2004,10316,RATTC,Tarte au sucre,39.4,70,0

200410,10/28/2004,10316,RATTC,Jack's New England Clam Chowder,7.7,10,0

200410,10/31/2004,10317,LONEP,Chai,14.4,20,0

200411,11/1/2004,10318,ISLAT,Jack's New England Clam Chowder,7.7,20,0

200411,11/1/2004,10318,ISLAT,Lakkalikööri,14.4,6,0

200411,11/2/2004,10319,TORTU,Alice Mutton,31.2,8,0

200411,11/2/2004,10319,TORTU,Rössle Sauerkraut,36.4,14,0

200411,11/2/2004,10319,TORTU,Lakkalikööri,14.4,30,0

200411,11/3/2004,10320,WARTH,Fløtemysost,17.2,30,0

200411,11/3/2004,10321,ISLAT,Steeleye Stout,14.4,10,0

200411,11/4/2004,10322,PERIC,Filo Mix,5.6,20,0

200411,11/7/2004,10323,KOENE,NuNuCa Nuß-Nougat-Creme,11.2,4,0

200411,11/7/2004,10323,KOENE,Chartreuse verte,14.4,4,0

200411,11/7/2004,10323,KOENE,Genen Shouyu,12.4,5,0

200411,11/8/2004,10324,SAVEA,Pavlova,13.9,21,0.15

200411,11/8/2004,10324,SAVEA,Steeleye Stout,14.4,70,0.15

200411,11/8/2004,10324,SAVEA,Spegesild,9.6,30,0

200411,11/8/2004,10324,SAVEA,Raclette Courdavault,44,40,0.15

200411,11/8/2004,10324,SAVEA,Vegie-spread,35.1,80,0.15

200411,11/9/2004,10325,KOENE,Konbu,4.8,12,0

200411,11/9/2004,10325,KOENE,Mozzarella di Giovanni,27.8,40,0

200411,11/9/2004,10325,KOENE,Tofu,18.6,9,0

200411,11/9/2004,10325,KOENE,Grandma's Boysenberry Spread,20,6,0

200411,11/9/2004,10325,KOENE,Gorgonzola Telino,10,4,0

200411,11/10/2004,10326,BOLID,Chef Anton's Cajun Seasoning,17.6,24,0

200411,11/10/2004,10326,BOLID,Ravioli Angelo,15.6,16,0

200411,11/10/2004,10326,BOLID,Rhönbräu Klosterbier,6.2,50,0

200411,11/11/2004,10327,FOLKO,Escargots de Bourgogne,10.6,30,0.2

200411,11/11/2004,10327,FOLKO,Chang,15.2,25,0.2

200411,11/11/2004,10327,FOLKO,Queso Cabrales,16.8,50,0.2

200411,11/11/2004,10327,FOLKO,Nord-Ost Matjeshering,20.7,35,0.2

200411,11/14/2004,10328,FURIB,Scottish Longbreads,10,10,0

200411,11/14/2004,10328,FURIB,Raclette Courdavault,44,9,0

200411,11/14/2004,10328,FURIB,Louisiana Fiery Hot Pepper Sauce,16.8,40,0

200411,11/15/2004,10329,SPLIR,Teatime Chocolate Biscuits,7.3,10,0.05

200411,11/15/2004,10329,SPLIR,Nord-Ost Matjeshering,20.7,8,0.05

200411,11/15/2004,10329,SPLIR,Côte de Blaye,210.8,20,0.05

200411,11/15/2004,10329,SPLIR,Gnocchi di nonna Alice,30.4,12,0.05

200411,11/16/2004,10330,LILAS,Gumbär Gummibärchen,24.9,50,0.15

200411,11/16/2004,10330,LILAS,Mozzarella di Giovanni,27.8,25,0.15

200411,11/16/2004,10331,BONAP,Tourtière,5.9,15,0

200411,11/17/2004,10332,MEREP,Zaanse koeken,7.6,16,0.2

200411,11/17/2004,10332,MEREP,Singaporean Hokkien Fried Mee,11.2,10,0.2

200411,11/17/2004,10332,MEREP,Carnarvon Tigers,50,40,0.2

200411,11/18/2004,10333,WARTH,Tofu,18.6,10,0

200411,11/18/2004,10333,WARTH,Sir Rodney's Scones,8,10,0.1

200411,11/18/2004,10333,WARTH,Fløtemysost,17.2,40,0.1

200411,11/21/2004,10334,VICTE,Filo Mix,5.6,8,0

200411,11/21/2004,10334,VICTE,Scottish Longbreads,10,10,0

200411,11/22/2004,10335,HUNGO,Mascarpone Fabioli,25.6,6,0.2

200411,11/22/2004,10335,HUNGO,Manjimup Dried Apples,42.4,48,0.2

200411,11/22/2004,10335,HUNGO,Gorgonzola Telino,10,25,0.2

200411,11/22/2004,10335,HUNGO,Chang,15.2,7,0.2

200411,11/23/2004,10336,PRINI,Chef Anton's Cajun Seasoning,17.6,18,0.1

200411,11/24/2004,10337,FRANK,Tunnbröd,7.2,40,0

200411,11/24/2004,10337,FRANK,Gumbär Gummibärchen,24.9,24,0

200411,11/24/2004,10337,FRANK,Inlagd Sill,15.2,20,0

200411,11/24/2004,10337,FRANK,Gravad lax,20.8,28,0

200411,11/24/2004,10337,FRANK,Mozzarella di Giovanni,27.8,25,0

200411,11/25/2004,10338,OLDWO,Nord-Ost Matjeshering,20.7,15,0

200411,11/25/2004,10338,OLDWO,Alice Mutton,31.2,20,0

200411,11/28/2004,10339,MEREP,Tarte au sucre,39.4,28,0

200411,11/28/2004,10339,MEREP,Alice Mutton,31.2,70,0.05

200411,11/28/2004,10339,MEREP,Chef Anton's Cajun Seasoning,17.6,10,0

200411,11/29/2004,10340,BONAP,Carnarvon Tigers,50,20,0.05

200411,11/29/2004,10340,BONAP,Jack's New England Clam Chowder,7.7,12,0.05

200411,11/29/2004,10340,BONAP,Ipoh Coffee,36.8,40,0.05

200411,11/29/2004,10341,SIMOB,Geitost,2,8,0

200411,11/29/2004,10341,SIMOB,Raclette Courdavault,44,9,0.15

200411,11/30/2004,10342,FRANK,Inlagd Sill,15.2,40,0.2

200411,11/30/2004,10342,FRANK,Pâté chinois,19.2,40,0.2

200411,11/30/2004,10342,FRANK,Gorgonzola Telino,10,56,0.2

200411,11/30/2004,10342,FRANK,Chang,15.2,24,0.2

200412,12/1/2004,10343,LEHMS,Scottish Longbreads,10,4,0.05

200412,12/1/2004,10343,LEHMS,Lakkalikööri,14.4,15,0

200412,12/1/2004,10343,LEHMS,Wimmers gute Semmelknödel,26.6,50,0

200412,12/2/2004,10344,WHITC,Chef Anton's Cajun Seasoning,17.6,35,0

200412,12/2/2004,10344,WHITC,Northwoods Cranberry Sauce,32,70,0.25

200412,12/5/2004,10345,QUICK,Northwoods Cranberry Sauce,32,70,0

200412,12/5/2004,10345,QUICK,Teatime Chocolate Biscuits,7.3,80,0

200412,12/5/2004,10345,QUICK,Singaporean Hokkien Fried Mee,11.2,9,0

200412,12/6/2004,10346,RATTC,Alice Mutton,31.2,36,0.1

200412,12/6/2004,10346,RATTC,Gnocchi di nonna Alice,30.4,20,0

200412,12/7/2004,10347,FAMIA,Chartreuse verte,14.4,50,0.15

200412,12/7/2004,10347,FAMIA,Boston Crab Meat,14.7,4,0

200412,12/7/2004,10347,FAMIA,Rhönbräu Klosterbier,6.2,6,0.15

200412,12/7/2004,10347,FAMIA,NuNuCa Nuß-Nougat-Creme,11.2,10,0

200412,12/8/2004,10348,WANDK,Chai,14.4,15,0.15

200412,12/8/2004,10348,WANDK,Tunnbröd,7.2,25,0

200412,12/9/2004,10349,SPLIR,Tourtière,5.9,24,0

200412,12/12/2004,10350,LAMAI,Gudbrandsdalsost,28.8,18,0.1

200412,12/12/2004,10350,LAMAI,Valkoinen suklaa,13,15,0.1

200412,12/12/2004,10351,ERNSH,Louisiana Fiery Hot Pepper Sauce,16.8,10,0.05

200412,12/12/2004,10351,ERNSH,Gula Malacca,15.5,77,0.05

200412,12/12/2004,10351,ERNSH,Côte de Blaye,210.8,20,0.05

200412,12/12/2004,10351,ERNSH,Jack's New England Clam Chowder,7.7,13,0

200412,12/13/2004,10352,FURIB,Guaraná Fantástica,3.6,10,0

200412,12/13/2004,10352,FURIB,Tourtière,5.9,20,0.15

200412,12/14/2004,10353,PICCO,Queso Cabrales,16.8,12,0.2

200412,12/14/2004,10353,PICCO,Côte de Blaye,210.8,50,0.2

200412,12/15/2004,10354,PERIC,Thüringer Rostbratwurst,99,4,0

200412,12/15/2004,10354,PERIC,Chai,14.4,12,0

200412,12/16/2004,10355,AROUT,Ravioli Angelo,15.6,25,0

200412,12/16/2004,10355,AROUT,Guaraná Fantástica,3.6,25,0

200412,12/19/2004,10356,WANDK,Gorgonzola Telino,10,30,0

200412,12/19/2004,10356,WANDK,Pâté chinois,19.2,12,0

200412,12/19/2004,10356,WANDK,Gudbrandsdalsost,28.8,20,0

200412,12/20/2004,10357,LILAS,Ikura,24.8,30,0.2

200412,12/20/2004,10357,LILAS,Gumbär Gummibärchen,24.9,16,0

200412,12/20/2004,10357,LILAS,Camembert Pierrot,27.2,8,0.2

200412,12/21/2004,10358,LAMAI,Sasquatch Ale,11.2,10,0.05

200412,12/21/2004,10358,LAMAI,Inlagd Sill,15.2,20,0.05

200412,12/21/2004,10358,LAMAI,Guaraná Fantástica,3.6,10,0.05

200412,12/22/2004,10359,SEVES,Gorgonzola Telino,10,70,0.05

200412,12/22/2004,10359,SEVES,Camembert Pierrot,27.2,80,0.05

200412,12/22/2004,10359,SEVES,Pavlova,13.9,56,0.05

200412,12/23/2004,10360,BLONP,Rössle Sauerkraut,36.4,30,0

200412,12/23/2004,10360,BLONP,Thüringer Rostbratwurst,99,35,0

200412,12/23/2004,10360,BLONP,Côte de Blaye,210.8,10,0

200412,12/23/2004,10360,BLONP,Maxilaku,16,35,0

200412,12/23/2004,10360,BLONP,Tourtière,5.9,28,0

200412,12/23/2004,10361,QUICK,Chartreuse verte,14.4,54,0.1

200412,12/23/2004,10361,QUICK,Camembert Pierrot,27.2,55,0.1

200412,12/26/2004,10362,BONAP,NuNuCa Nuß-Nougat-Creme,11.2,50,0

200412,12/26/2004,10362,BONAP,Manjimup Dried Apples,42.4,20,0

200412,12/26/2004,10362,BONAP,Tourtière,5.9,24,0

200412,12/27/2004,10363,DRACD,Gorgonzola Telino,10,20,0

200412,12/27/2004,10363,DRACD,Rhönbräu Klosterbier,6.2,12,0

200412,12/27/2004,10363,DRACD,Lakkalikööri,14.4,12,0

200412,12/27/2004,10364,EASTC,Gudbrandsdalsost,28.8,30,0

200412,12/27/2004,10364,EASTC,Fløtemysost,17.2,5,0

200412,12/28/2004,10365,ANTON,Queso Cabrales,16.8,24,0

200412,12/29/2004,10366,GALED,Louisiana Fiery Hot Pepper Sauce,16.8,5,0

200412,12/29/2004,10366,GALED,Original Frankfurter grüne Soße,10.4,5,0

200412,12/29/2004,10367,VAFFE,Sasquatch Ale,11.2,36,0

200412,12/29/2004,10367,VAFFE,Tourtière,5.9,18,0

200412,12/29/2004,10367,VAFFE,Louisiana Fiery Hot Pepper Sauce,16.8,15,0

200412,12/29/2004,10367,VAFFE,Original Frankfurter grüne Soße,10.4,7,0

200412,12/30/2004,10368,ERNSH,Wimmers gute Semmelknödel,26.6,35,0.1

200412,12/30/2004,10368,ERNSH,Sir Rodney's Scones,8,5,0.1

200412,12/30/2004,10368,ERNSH,Rössle Sauerkraut,36.4,13,0.1

200412,12/30/2004,10368,ERNSH,Ravioli Angelo,15.6,25,0

 

Upon examination of the file, you can see that there is three months worth of data (several records for October, November, and December 2004, indicated by the first field values 200410, 200411, and 200412, respectively). We could say that the major control field is the Year-Month, forming control groups for the sets of records for each month (all the records with the value of 200410 in the first field is one control group, as is all the records with the value of 200411 in the first field,  as is all the records with the value of 200412 in the first field). In a printed report, management would want to see subtotals for each month.

 

Within each month, we see that there are (or can be) several records for each date (for example, only one record for 10/3/2004, but four records for 10/10/2004, and six records for 12/12/2004). It would follow that management would want to see subtotals for each date in addition to the monthly totals.

 

A final breakdown would be by order number. An order can consist of more than one product; an examination of the data bears this out. For example, order number 10296 on 10/4/2004 consisted of three products; order number 10309 on 10/20/2004 consisted of five products. So we can see that subtotals for each order would also be required on the report.

 

The last sample program in this article will show you how to create a report with these three levels of control break subtotals (year-month, date, and order number) in addition to grand totals. A note on terminology: when you have two or more control breaks, the highest level break (in this case, year-month) is considered the major control break; the lowest level break (in this case, order number) is considered the minor control break; and any breaks in between (in this case, the date) are considered intermediate control breaks.

 

Four sample programs will be presented in this article, each one a variation on the other. The four sample programs will respectively produce:

·         Sales report with no control breaks (grand totals only)

·         Sales report with control break totals for year-month (as well as the grand totals)

·         Sales report with control break totals for both year-month and date (as well as the grand totals)

·         Sales report with control break totals for year-month, date, and order number (as well as the grand totals)

 

Excerpts of the outputs of each of the four sample programs are shown below. This will be followed by a discussion of the programming considerations for each, as well as links to download each sample program.

 

Sample Program 1

 

Sample Program 1 processes the Northwind input file described above and produces a sales report with grand totals only (no control breaks or subtotals). Excerpts of the output are shown below. Report headings, including the date, time, page number, and column headings are printed at the top of each page. A detail line is printed for each input record, consisting of the year-month, order date, order number, customer ID, product description, unit price, and quantity fields from the input record, along with the calculated fields extended price, discount amount, and sale total (the discount percentage from the input record is used for the calculations but is not itself printed on the report). Grand totals for the three calculated fields are printed in boldface type at the end of the report.

    

 

    

     Print Date: 04/25/05                              THEVBPROGRAMMER.COM                                         Page:  1

     Print Time: 21:36:35                         NORTHWIND TRADERS SALES REPORT

                                            WITH NO CONTROL BREAKS (GRAND TOTALS ONLY)

 

     YEAR/                  ORDER  CUST                                      UNIT           EXTENDED    DISCOUNT

     MONTH      ORDER DATE  NUMBER  ID    PRODUCT DESCRIPTION               PRICE   QTY      PRICE       AMOUNT   SALE TOTAL

     -----      ----------  ------  --    -------------------               -----   ---      -----       ------   ----------

     200410     10/3/2004   10295  VINET  Gnocchi di nonna Alice            30.40     4      $121.60       $0.00     $121.60

     200410     10/4/2004   10296  LILAS  Pavlova                           13.90    30      $417.00       $0.00     $417.00

     200410     10/4/2004   10296  LILAS  Gudbrandsdalsost                  28.80    15      $432.00       $0.00     $432.00

     200410     10/4/2004   10296  LILAS  Queso Cabrales                    16.80    12      $201.60       $0.00     $201.60

     200410     10/5/2004   10297  BLONP  Mozzarella di Giovanni            27.80    20      $556.00       $0.00     $556.00

     200410     10/5/2004   10297  BLONP  Chartreuse verte                  14.40    60      $864.00       $0.00     $864.00

     200410     10/6/2004   10298  HUNGO  Chang                             15.20    40      $608.00       $0.00     $608.00

     200410     10/6/2004   10298  HUNGO  Inlagd Sill                       15.20    40      $608.00     $152.00     $456.00

     200410     10/6/2004   10298  HUNGO  Raclette Courdavault              44.00    30    $1,320.00     $330.00     $990.00

     200410     10/6/2004   10298  HUNGO  Tarte au sucre                    39.40    15      $591.00       $0.00     $591.00

.  .  .

     200412     12/29/2004  10366  GALED  Louisiana Fiery Hot Pepper Sauce  16.80     5       $84.00       $0.00      $84.00

     200412     12/29/2004  10366  GALED  Original Frankfurter grüne Soße   10.40     5       $52.00       $0.00      $52.00

     200412     12/29/2004  10367  VAFFE  Sasquatch Ale                     11.20    36      $403.20       $0.00     $403.20

     200412     12/29/2004  10367  VAFFE  Tourtière                          5.90    18      $106.20       $0.00     $106.20

     200412     12/29/2004  10367  VAFFE  Louisiana Fiery Hot Pepper Sauce  16.80    15      $252.00       $0.00     $252.00

     200412     12/29/2004  10367  VAFFE  Original Frankfurter grüne Soße   10.40     7       $72.80       $0.00      $72.80

     200412     12/30/2004  10368  ERNSH  Wimmers gute Semmelknödel         26.60    35      $931.00      $93.10     $837.90

     200412     12/30/2004  10368  ERNSH  Sir Rodney's Scones                8.00     5       $40.00       $4.00      $36.00

     200412     12/30/2004  10368  ERNSH  Rössle Sauerkraut                 36.40    13      $473.20      $47.32     $425.88

     200412     12/30/2004  10368  ERNSH  Ravioli Angelo                    15.60    25      $390.00       $0.00     $390.00

 

                                                                           GRAND TOTALS: $118,543.60   $9,046.43 $109,497.17

 

 

 

Sample Program 2

 

Sample Program 2 processes the same input file as Sample Program 1. The difference in the output is that Sample Program 2 produces subtotals based on the year-month field in addition to the grand totals. And instead of showing the year-month on each detail line, it is "factored out" and printed on a separate "sub-heading" line preceding the detail lines for that year-month. Furthermore, the year-month is formatted with the month name and year (i.e., OCTOBER, 2004 as opposed to 200410).

 

Excerpts of the output report produced by Sample Program 2 are shown below. Note how the month/year is printed on a separate line in bold at the start of each month/year group. Note also that subtotals for the month/year are printed in bold whenever there is a change (or break) in the month/year groups. As in Sample Program 1, grand totals are printed in bold at the end of the report.

 

     Print Date: 04/25/05                              THEVBPROGRAMMER.COM                                         Page:  1

     Print Time: 21:51:39                         NORTHWIND TRADERS SALES REPORT

                                                   WITH CONTROL BREAK ON MONTH

 

                            ORDER  CUST                                      UNIT           EXTENDED    DISCOUNT

                ORDER DATE  NUMBER  ID    PRODUCT DESCRIPTION               PRICE   QTY      PRICE       AMOUNT   SALE TOTAL

                ----------  ------  --    -------------------               -----   ---      -----       ------   ----------

 

     OCTOBER, 2004

 

                10/3/2004   10295  VINET  Gnocchi di nonna Alice            30.40     4      $121.60       $0.00     $121.60

                10/4/2004   10296  LILAS  Pavlova                           13.90    30      $417.00       $0.00     $417.00

                10/4/2004   10296  LILAS  Gudbrandsdalsost                  28.80    15      $432.00       $0.00     $432.00

                10/4/2004   10296  LILAS  Queso Cabrales                    16.80    12      $201.60       $0.00     $201.60

                10/5/2004   10297  BLONP  Mozzarella di Giovanni            27.80    20      $556.00       $0.00     $556.00

                10/5/2004   10297  BLONP  Chartreuse verte                  14.40    60      $864.00       $0.00     $864.00

                10/6/2004   10298  HUNGO  Chang                             15.20    40      $608.00       $0.00     $608.00

                10/6/2004   10298  HUNGO  Inlagd Sill                       15.20    40      $608.00     $152.00     $456.00

                10/6/2004   10298  HUNGO  Raclette Courdavault              44.00    30    $1,320.00     $330.00     $990.00

 

. . .

 

                10/28/2004  10316  RATTC  Tarte au sucre                    39.40    70    $2,758.00       $0.00   $2,758.00

                10/28/2004  10316  RATTC  Jack's New England Clam Chowder    7.70    10       $77.00       $0.00      $77.00

                10/31/2004  10317  LONEP  Chai                              14.40    20      $288.00       $0.00     $288.00

                                                                                         ----------- ----------- -----------                                                                      

                                                               TOTALS FOR OCTOBER, 2004:  $27,636.00   $1,254.60  $26,381.40

 

     NOVEMBER, 2004

 

                11/1/2004   10318  ISLAT  Jack's New England Clam Chowder    7.70    20      $154.00       $0.00     $154.00

                11/1/2004   10318  ISLAT  Lakkalikööri                      14.40     6       $86.40       $0.00      $86.40

                11/2/2004   10319  TORTU  Alice Mutton                      31.20     8      $249.60       $0.00     $249.60

                11/2/2004   10319  TORTU  Rössle Sauerkraut                 36.40    14      $509.60       $0.00     $509.60

                11/2/2004   10319  TORTU  Lakkalikööri                      14.40    30      $432.00       $0.00     $432.00

 

 

. . .

 

                11/30/2004  10342  FRANK  Inlagd Sill                       15.20    40      $608.00     $121.60     $486.40

                11/30/2004  10342  FRANK  Pâté chinois                      19.20    40      $768.00     $153.60     $614.40

                11/30/2004  10342  FRANK  Gorgonzola Telino                 10.00    56      $560.00     $112.00     $448.00

                11/30/2004  10342  FRANK  Chang                             15.20    24      $364.80      $72.96     $291.84

                                                                                         ----------- ----------- -----------                                                                     

                                                              TOTALS FOR NOVEMBER, 2004:  $39,617.60   $3,685.88  $35,931.72

 

     DECEMBER, 2004

 

                12/1/2004   10343  LEHMS  Scottish Longbreads               10.00     4       $40.00       $2.00      $38.00

                12/1/2004   10343  LEHMS  Lakkalikööri                      14.40    15      $216.00       $0.00     $216.00

                12/1/2004   10343  LEHMS  Wimmers gute Semmelknödel         26.60    50    $1,330.00       $0.00   $1,330.00

 

. . .

 

                12/30/2004  10368  ERNSH  Wimmers gute Semmelknödel         26.60    35      $931.00      $93.10     $837.90

                12/30/2004  10368  ERNSH  Sir Rodney's Scones                8.00     5       $40.00       $4.00      $36.00

                12/30/2004  10368  ERNSH  Rössle Sauerkraut                 36.40    13      $473.20      $47.32     $425.88

                12/30/2004  10368  ERNSH  Ravioli Angelo                    15.60    25      $390.00       $0.00     $390.00

                                                                                         ----------- ----------- -----------                                                                     

                                                              TOTALS FOR DECEMBER, 2004:  $51,290.00   $4,105.96  $47,184.05

 

                                                                           GRAND TOTALS: $118,543.60   $9,046.43 $109,497.17

 

 

Sample Program 3

 

Sample Program 3 builds on Sample Program 2. In addition to the year-month control break implemented in Sample Program 2, Sample Program 3 implements an additional control break on the order date.

 

Excerpts of the output report produced by Sample Program 3 are shown below. Note that the report now includes subtotals by order date in addition to subtotals by year-month.   Note also that for each group of items printed where the order date is the same, the date is only displayed on the first line of that group; it is blanked out for the remaining lines. This technique is called group indication. (We are also "group indicating" by printing the year/month in its own sub-heading.) The idea behind group indication is to improve readability of the report and suppress redundant information.

 

As in both Sample Program 2 and Sample Program 1, grand totals are printed in bold at the end of the report.

 

     Print Date: 05/01/05                              THEVBPROGRAMMER.COM                                         Page:  1

     Print Time: 14:08:22                         NORTHWIND TRADERS SALES REPORT

                                              WITH CONTROL BREAKS ON DATE AND MONTH

 

                            ORDER  CUST                                      UNIT           EXTENDED    DISCOUNT

                ORDER DATE  NUMBER  ID    PRODUCT DESCRIPTION               PRICE   QTY      PRICE       AMOUNT   SALE TOTAL

                ----------  ------  --    -------------------               -----   ---      -----       ------   ----------

 

     OCTOBER, 2004

 

                10/3/2004   10295  VINET  Gnocchi di nonna Alice            30.40     4      $121.60       $0.00     $121.60

                                                                                         ----------- ----------- -----------

                                                                   TOTALS FOR 10/3/2004:     $121.60       $0.00     $121.60

 

                10/4/2004   10296  LILAS  Pavlova                           13.90    30      $417.00       $0.00     $417.00

                            10296  LILAS  Gudbrandsdalsost                  28.80    15      $432.00       $0.00     $432.00

                            10296  LILAS  Queso Cabrales                    16.80    12      $201.60       $0.00     $201.60

                                                                                         ----------- ----------- -----------

                                                                   TOTALS FOR 10/4/2004:   $1,050.60       $0.00   $1,050.60

 

                10/5/2004   10297  BLONP  Mozzarella di Giovanni            27.80    20      $556.00       $0.00     $556.00

                            10297  BLONP  Chartreuse verte                  14.40    60      $864.00       $0.00     $864.00

                                                                                         ----------- ----------- -----------

                                                                   TOTALS FOR 10/5/2004:   $1,420.00       $0.00   $1,420.00

 

                10/6/2004   10298  HUNGO  Chang                             15.20    40      $608.00       $0.00     $608.00

                            10298  HUNGO  Inlagd Sill                       15.20    40      $608.00     $152.00     $456.00

                            10298  HUNGO  Raclette Courdavault              44.00    30    $1,320.00     $330.00     $990.00

                            10298  HUNGO  Tarte au sucre                    39.40    15      $591.00       $0.00     $591.00

                                                                                         ----------- ----------- -----------

                                                                   TOTALS FOR 10/6/2004:   $3,127.00     $482.00   $2,645.00

 

. . .

 

     Print Date: 05/01/05                              THEVBPROGRAMMER.COM                                         Page: 11

     Print Time: 14:08:22                         NORTHWIND TRADERS SALES REPORT

                                              WITH CONTROL BREAKS ON DATE AND MONTH

 

                            ORDER  CUST                                      UNIT           EXTENDED    DISCOUNT

                ORDER DATE  NUMBER  ID    PRODUCT DESCRIPTION               PRICE   QTY      PRICE       AMOUNT   SALE TOTAL

                ----------  ------  --    -------------------               -----   ---      -----       ------   ----------

 

                                                                                         ----------- ----------- -----------

                                                                  TOTALS FOR 12/29/2004:     $970.20       $0.00     $970.20

 

                12/30/2004  10368  ERNSH  Wimmers gute Semmelknödel         26.60    35      $931.00      $93.10     $837.90

                            10368  ERNSH  Sir Rodney's Scones                8.00     5       $40.00       $4.00      $36.00

                            10368  ERNSH  Rössle Sauerkraut                 36.40    13      $473.20      $47.32     $425.88

                            10368  ERNSH  Ravioli Angelo                    15.60    25      $390.00       $0.00     $390.00

                                                                                         ----------- ----------- -----------

                                                                  TOTALS FOR 12/30/2004:   $1,834.20     $144.42   $1,689.78

 

                                                              TOTALS FOR DECEMBER, 2004:  $51,290.00   $4,105.96  $47,184.05

 

                                                                           GRAND TOTALS: $118,543.60   $9,046.43 $109,497.17

 

 

 

Sample Program 4

 

Sample Program 4 builds on Sample Program 3. In addition to the year-month and order date control breaks implemented in Sample Program 3, Sample Program 4 implements an additional control break on order number.

 

Excerpts of the output report produced by Sample Program 4 are shown below. Note that the report now includes subtotals by order number in addition to subtotals by year-month and order date.  Note also that the technique of group indication has been applied to the order number (and its associated field, the customer ID, because an order is always associated with a single customer). The order number and customer ID are displayed on the first line of the group of records associated with an order, but blanked out (suppressed) for the remaining lines of the group.  As mentioned earlier, the idea behind group indication is to improve readability of the report and suppress redundant information.

 

As in both Sample Programs 1, 2, and 3, grand totals are printed in bold at the end of the report.

 

     Print Date: 04/25/05                              THEVBPROGRAMMER.COM                                         Page: 14

     Print Time: 22:03:35                         NORTHWIND TRADERS SALES REPORT

                                              WITH BREAKS ON ORDER, DATE, AND MONTH

 

                            ORDER  CUST                                      UNIT           EXTENDED    DISCOUNT

                ORDER DATE  NUMBER  ID    PRODUCT DESCRIPTION               PRICE   QTY      PRICE       AMOUNT   SALE TOTAL

                ----------  ------  --    -------------------               -----   ---      -----       ------   ----------

 

                                                                  TOTALS FOR 12/23/2004:   $9,663.80     $227.36   $9,436.44

 

                12/26/2004  10362  BONAP  NuNuCa Nuß-Nougat-Creme           11.20    50      $560.00       $0.00     $560.00

                                          Manjimup Dried Apples             42.40    20      $848.00       $0.00     $848.00

                                          Tourtière                          5.90    24      $141.60       $0.00     $141.60

                                                                                         ----------- ----------- -----------

                                                          TOTALS FOR ORDER NUMBER 10362:   $1,549.60       $0.00   $1,549.60

 

                                                                  TOTALS FOR 12/26/2004:   $1,549.60       $0.00   $1,549.60

 

                12/27/2004  10363  DRACD  Gorgonzola Telino                 10.00    20      $200.00       $0.00     $200.00

                                          Rhönbräu Klosterbier               6.20    12       $74.40       $0.00      $74.40

                                          Lakkalikööri                      14.40    12      $172.80       $0.00     $172.80

                                                                                         ----------- ----------- -----------

                                                          TOTALS FOR ORDER NUMBER 10363:     $447.20       $0.00     $447.20

 

                            10364  EASTC  Gudbrandsdalsost                  28.80    30      $864.00       $0.00     $864.00

                                          Fløtemysost                       17.20     5       $86.00       $0.00      $86.00

                                                                                         ----------- ----------- -----------

                                                          TOTALS FOR ORDER NUMBER 10364:     $950.00       $0.00     $950.00

 

                                                                  TOTALS FOR 12/27/2004:   $1,397.20       $0.00   $1,397.20

 

                12/28/2004  10365  ANTON  Queso Cabrales                    16.80    24      $403.20       $0.00     $403.20

                                                                                         ----------- ----------- -----------

                                                          TOTALS FOR ORDER NUMBER 10365:     $403.20       $0.00     $403.20

 

                                                                  TOTALS FOR 12/28/2004:     $403.20       $0.00     $403.20

 

                12/29/2004  10366  GALED  Louisiana Fiery Hot Pepper Sauce  16.80     5       $84.00       $0.00      $84.00

                                          Original Frankfurter grüne Soße   10.40     5       $52.00       $0.00      $52.00

                                                                                         ----------- ----------- -----------

                                                          TOTALS FOR ORDER NUMBER 10366:     $136.00       $0.00     $136.00

 

                            10367  VAFFE  Sasquatch Ale                     11.20    36      $403.20       $0.00     $403.20

                                          Tourtière                          5.90    18      $106.20       $0.00     $106.20

                                          Louisiana Fiery Hot Pepper Sauce  16.80    15      $252.00       $0.00     $252.00

                                          Original Frankfurter grüne Soße   10.40     7       $72.80       $0.00      $72.80

 

 

     Print Date: 04/25/05                              THEVBPROGRAMMER.COM                                         Page: 15

     Print Time: 22:03:35                         NORTHWIND TRADERS SALES REPORT

                                              WITH BREAKS ON ORDER, DATE, AND MONTH

 

                            ORDER  CUST                                      UNIT           EXTENDED    DISCOUNT

                ORDER DATE  NUMBER  ID    PRODUCT DESCRIPTION               PRICE   QTY      PRICE       AMOUNT   SALE TOTAL

                ----------  ------  --    -------------------               -----   ---      -----       ------   ----------

 

                                                                                         ----------- ----------- -----------

                                                          TOTALS FOR ORDER NUMBER 10367:     $834.20       $0.00     $834.20

 

                                                                  TOTALS FOR 12/29/2004:     $970.20       $0.00     $970.20

 

                12/30/2004  10368  ERNSH  Wimmers gute Semmelknödel         26.60    35      $931.00      $93.10     $837.90

                                          Sir Rodney's Scones                8.00     5       $40.00       $4.00      $36.00

                                          Rössle Sauerkraut                 36.40    13      $473.20      $47.32     $425.88

                                          Ravioli Angelo                    15.60    25      $390.00       $0.00     $390.00

                                                                                         ----------- ----------- -----------

                                                          TOTALS FOR ORDER NUMBER 10368:   $1,834.20     $144.42   $1,689.78

 

                                                                  TOTALS FOR 12/30/2004:   $1,834.20     $144.42   $1,689.78

 

                                                              TOTALS FOR DECEMBER, 2004:  $51,290.00   $4,105.96  $47,184.05

 

                                                                           GRAND TOTALS: $118,543.60   $9,046.43 $109,497.17

 

When programming reports "by hand" like this, you must account for "report printing logic", such as printing headings on every page, tracking the page count, knowing when to perform a page break by tracking the number of lines currently printed on the page, designing the detail lines so that all fields fit within the maximum number of characters per line, etc.  By using a monospaced font like Courier New, we can determine the number of characters that will fit on one line and the number of lines that will fit on one page. Note: A monospaced font is one in which each character takes up the same amount of space, like a typewriter. Examples of monospaced fonts are Courier and Lucida Console. This is contrasted with proportional fonts, where the width of each letter varies (for example, a "W" is much wider than an "i"). Examples of proportional fonts are Times New Roman and Arial (this font). By knowing the number of characters that can fit on one line, we can plan the format of the data to be presented. By knowing the number of lines that will fit on one page, we can use logic to perform page breaks and print headings when a page fills up with data.

 

Programmers in the days of yore used a printer spacing chart form to design printouts. An actual printer spacing chart form might be hard to come by these days, but any type of form with grid lines like this would get the job done. A freeware Excel template file that replicates the printer spacing chart (as well as other "old-school" forms) is available. To use the Excel template, either print the blank sheet and pencil your design in, or copy the template to a separate work file and enter your design directly in the cells of the Excel template worksheet. The latter method was used to produce the example shown below. The Excel template file, as well as the file containing the sample chart below, can be downloaded here.

 

 

In these sample programs, the reports are laid out in landscape orientation, and Courier New 9 point is the font of choice. Taking margins into consideration, it was determined that 118 characters per line was an appropriate limit.  The sample programs use the PrintDocument object to handle printing, and in the PrintPage event, a calculation is performed to yield the maximum number of lines per page based on the font size. In that the PrintPage event fires whenever a new page needs to be printed, we can use that information to keep the page count.

 

A major challenge when converting these sample programs over to VB.NET from their classic VB counterparts was the use of the PrintDocument object, which is event-driven. I found that this clashed with the procedure-oriented way that these report programs need to be written; where the control break logic is intertwined with the report logic. The solution I came up with was to write all the report lines (except for the page heading lines) to an array, then print from the array.

 

I will now go over some of the logic involved in the sample programs.

 

First, all of the sample programs use the Input statement to process the input text file (recall that the Input statement reads one field at a time from a comma-delimited file; and you typically use a set of Input statements to read one "record's worth" of fields at a time). In the sample program, a set of form-level variables to represent the fields from the input record is declared:

 

    Private mstrYearMonth As String

    Private mstrOrderNbr As String

    Private mstrOrderDate As String

    Private mstrCustID As String

    Private mstrProdDesc As String

    Private msngUnitPrice As Single

    Private mintQty As Integer

    Private msngDiscountPct As Single

 

The form-level variable mintSalesFileNbr is used for the file number. So to read a "record" from the input file, the following set of Input statements is used:

 

Input(mintSalesFileNbr, mstrYearMonth)

Input(mintSalesFileNbr, mstrOrderDate)

Input(mintSalesFileNbr, mstrOrderNbr)

Input(mintSalesFileNbr, mstrCustID)

Input(mintSalesFileNbr, mstrProdDesc)

Input(mintSalesFileNbr, msngUnitPrice)

Input(mintSalesFileNbr, mintQty)

Input(mintSalesFileNbr, msngDiscountPct)

 

 

The Input statement detects end-of-file (EOF) at the same time the last record in the file is read. That is why a normal file processing loop looks like this:

 

Do Until EOF(FileNumber)

    Input(FileNumber, variable)

    'Process current record

Loop

 

However, for some algorithms (particularly control break processing), it is more convenient have the end of file condition triggered on a separate iteration of the processing loop. (Doing this we are basically delaying the testing for the end-of-file condition on the input text file until after the last record has been read.) This is the type of "input" or "read" loop that uses a priming read outside the loop; all subsequent reads occur at the bottom of the loop.  The pseudocode might be written as follows:

           

            READ A RECORD          ' priming read

            DO UNTIL EOF

                PROCESS THE RECORD

                READ A RECORD      ' second and subsequent reads

            LOOP

 

To simulate this behavior, the sample programs use the ReadSalesRecord Sub to read a record. The setup is as follows: a form-level Boolean variable named mblnEOF is declared, initialized to False. The ReadSalesRecord Sub is coded as follows:

 

    Private Sub ReadSalesRecord()

 

        If EOF(mintSalesFileNbr) Then

            mblnEOF = True

        Else

            Input(mintSalesFileNbr, mstrYearMonth)

            Input(mintSalesFileNbr, mstrOrderDate)

            Input(mintSalesFileNbr, mstrOrderNbr)

            Input(mintSalesFileNbr, mstrCustID)

            Input(mintSalesFileNbr, mstrProdDesc)

            Input(mintSalesFileNbr, msngUnitPrice)

            Input(mintSalesFileNbr, mintQty)

            Input(mintSalesFileNbr, msngDiscountPct)

        End If

 

    End Sub

 

And the loop to process the file looks like this:

 

        Call ReadSalesRecord()

 

        Do Until mblnEOF

 

           ' process the data for the current record here

            ' ...

            ' read the next record

            Call ReadSalesRecord()

 

        Loop

 

 

With this coding, when the last record is read, the system "knows" that EOF has occurred, but your processing loop won't "know" it until it calls the Sub one last time, at which point the condition EOF(mintSalesFileNbr) will be True, which will cause the form-level Boolean variable mblnEOF to be set to True.

 

The interface for all four sample programs is identical, except for the title bar description. The interface consists of two buttons, named btnPrint and btnExit, with all of the logic taking place behind btnPrint.

 

 

 

   

 

  

   

 

 

The code behind the "Print" button is identical for all four programs:

 

    Private Sub btnPrint_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnPrint.Click

 

        ' Generate the set of detail lines to be printed ...

        GenerateDetailLineArray()

 

        ' Declare an object for the PrintDocument class...

        Dim objPrintDocument As PrintDocument = New PrintDocument()

 

        ' set the orientation to Landscape ...

        objPrintDocument.DefaultPageSettings.Landscape = True

 

        ' Set the print font...

        mobjPrintFont = New Font("Courier New", 9)

 

        ' Add an event handler for the PrintPage event of the

        ' objPrintDocument object...

        AddHandler objPrintDocument.PrintPage, AddressOf objPrintDocument_PrintPage

 

        With PrintPreviewDialog1

            .FindForm().StartPosition = FormStartPosition.CenterScreen

            .Height = 600

            .Width = 800

            .PrintPreviewControl.Zoom = 0.75

            .Document = objPrintDocument

            .ShowDialog()

        End With

 

    End Sub

 

As mentioned earlier, I decided to resolve my control break logic / report logic conflict issues by "writing" the entire report (except for the page heading lines) to an array before doing any printing. This is taken care of in a Sub  called GenerateDetailLineArray. You can see from the code above that this is the first thing that is done. It is in the GenerateDetailLineArray Sub that the differences in logic between the four sample programs occurs.

 

In all cases, the array that is being populated is declared as follows:

 

Private mastrDetailLines() As String

 

Notes on Sample Program 1

 

In the first sample program (which does not do any control breaks; only grand totals are generated), the GenerateDetailLineArray is coded as follows:

 

    Private Sub GenerateDetailLineArray()

 

        Dim decExtPriceDtl As Decimal

        Dim decDiscAmtDtl As Decimal

        Dim decSaleTotDtl As Decimal

 

        Dim decExtPriceGrand As Decimal = 0

        Dim decDiscAmtGrand As Decimal = 0

        Dim decSaleTotGrand As Decimal = 0

 

        mintLineX = -1

        Erase mastrDetailLines

        mblnEOF = False

 

        ' open the customer data file ...

        mintSalesFileNbr = FreeFile()

        FileOpen(mintSalesFileNbr, mstrSalesFileName, OpenMode.Input)

        ' priming read

        Call ReadSalesRecord()

 

        ' main outer processing loop

        Do Until mblnEOF

 

            decExtPriceDtl = msngUnitPrice * mintQty

            decDiscAmtDtl = decExtPriceDtl * msngDiscountPct

            decSaleTotDtl = decExtPriceDtl - decDiscAmtDtl

 

            ' generate a detail line

            mintLineX += 1

            ReDim Preserve mastrDetailLines(mintLineX)

            mastrDetailLines(mintLineX) = Space(10) _

                                        & mstrOrderDate.PadRight(10) _

                                        & Space(1) _

                                        & mstrOrderNbr.PadRight(5) _

                                        & Space(2) _

                                        & mstrCustID.PadRight(5) _

                                        & Space(3) _

                                        & mstrProdDesc.PadRight(33) _

                                        & Space(1) _

                                        & FormatNumber(msngUnitPrice, 2).PadLeft(6) _

                                        & Space(3) _

                                        & mintQty.ToString.PadLeft(3) _

                                        & Space(2) _

                                        & FormatCurrency(decExtPriceDtl, 2).PadLeft(11) _

                                        & Space(1) _

                                        & FormatCurrency(decDiscAmtDtl, 2).PadLeft(11) _

                                        & Space(1) _

                                        & FormatCurrency(decSaleTotDtl, 2).PadLeft(11)

 

            ' "roll" the detail amounts to the grand totals

            decExtPriceGrand += decExtPriceDtl

            decDiscAmtGrand += decDiscAmtDtl

            decSaleTotGrand += decSaleTotDtl

 

            ' read the next record

            Call ReadSalesRecord()

 

        Loop

        ' end of outer processing loop - time to generate the grand totals

 

        mintLineX += 1

        ReDim Preserve mastrDetailLines(mintLineX)

        mastrDetailLines(mintLineX) = " "

 

        mintLineX += 1

        ReDim Preserve mastrDetailLines(mintLineX)

        mastrDetailLines(mintLineX) = Space(70) _

                                    & "GRAND TOTALS:" _

                                    & Space(1) _

                                    & FormatCurrency(decExtPriceGrand, 2).PadLeft(11) _

                                    & Space(1) _

                                    & FormatCurrency(decDiscAmtGrand, 2).PadLeft(11) _

                                    & Space(1) _

                                    & FormatCurrency(decSaleTotGrand, 2).PadLeft(11)

 

        ' close the input file

        FileClose(mintSalesFileNbr)

 

    End Sub

 

 

In the code above, the variables decExtPriceDtl, decDiscAmtDtl, and decSaleTotDtl are the variables used for the calculated fields (extended price, discount amount, and sale total). The variables decExtPriceGrand, decDiscAmtGrand, and decSaleTotGrand are the variables for the corresponding grand total. As each input record is read,  the calculated field variables are computed, a detail line is formatted and added to the array, and then the calculated values are added to their corresponding grand total variables. After the file has been processed, the grand total line is generated and added to the array.

 

Notes on Sample Program 2

 

In the second sample program (which does a control break by month in addition to generating grand totals), the GenerateDetailLineArray is coded as follows:

 

    Private Sub GenerateDetailLineArray()

 

        Dim strPrevMonth As String

 

        Dim decExtPriceDtl As Decimal

        Dim decDiscAmtDtl As Decimal

        Dim decSaleTotDtl As Decimal

 

        Dim decExtPriceMonth As Decimal

        Dim decDiscAmtMonth As Decimal

        Dim decSaleTotMonth As Decimal

 

        Dim decExtPriceGrand As Decimal

        Dim decDiscAmtGrand As Decimal

        Dim decSaleTotGrand As Decimal

 

        mintLineX = -1

        Erase mastrDetailLines

        mblnEOF = False

 

        ' open the customer data file ...

        mintSalesFileNbr = FreeFile()

        FileOpen(mintSalesFileNbr, mstrSalesFileName, OpenMode.Input)

        ' priming read

        Call ReadSalesRecord()

 

        ' main outer processing loop

        Do Until mblnEOF

 

            ' set up for month break

 

            strPrevMonth = mstrYearMonth

 

            mintLineX += 1

            ReDim Preserve mastrDetailLines(mintLineX)

            mastrDetailLines(mintLineX) = UCase(MonthName(CInt(Strings.Right(mstrYearMonth, 2)))) & ", " _

                                        & Strings.Left(mstrYearMonth, 4)

            mintLineX += 1

            ReDim Preserve mastrDetailLines(mintLineX)

            mastrDetailLines(mintLineX) = " "

 

            decExtPriceMonth = 0

            decDiscAmtMonth = 0

            decSaleTotMonth = 0

 

            ' inner loop 1 for month break

            Do Until (strPrevMonth <> mstrYearMonth) _

                  Or (mblnEOF)

 

                decExtPriceDtl = msngUnitPrice * mintQty

                decDiscAmtDtl = decExtPriceDtl * msngDiscountPct

                decSaleTotDtl = decExtPriceDtl - decDiscAmtDtl

 

                ' generate a detail line

                mintLineX += 1

                ReDim Preserve mastrDetailLines(mintLineX)

                mastrDetailLines(mintLineX) = Space(10) _

                                            & mstrOrderDate.PadRight(10) _

                                            & Space(1) _

                                            & mstrOrderNbr.PadRight(5) _

                                            & Space(2) _

                                            & mstrCustID.PadRight(5) _

                                            & Space(3) _

                                            & mstrProdDesc.PadRight(33) _

                                            & Space(1) _

                                            & FormatNumber(msngUnitPrice, 2).PadLeft(6) _

                                            & Space(3) _

                                            & mintQty.ToString.PadLeft(3) _

                                            & Space(2) _

                                            & FormatCurrency(decExtPriceDtl, 2).PadLeft(11) _

                                            & Space(1) _

                                            & FormatCurrency(decDiscAmtDtl, 2).PadLeft(11) _

                                            & Space(1) _

                                            & FormatCurrency(decSaleTotDtl, 2).PadLeft(11)

 

                ' "roll" the detail amounts to the month totals

                decExtPriceMonth += decExtPriceDtl

                decDiscAmtMonth += decDiscAmtDtl

                decSaleTotMonth += decSaleTotDtl

 

                ' read the next record

                Call ReadSalesRecord()

 

            Loop

 

            ' end of inner loop 1

            ' we have our break on month, so generate the month totals

 

            mintLineX += 1

            ReDim Preserve mastrDetailLines(mintLineX)

            mastrDetailLines(mintLineX) = Space(84) _

                                        & " ----------" _

                                        & Space(1) _

                                        & " ----------" _

                                        & Space(1) _

                                        & " ----------"

 

            mintLineX += 1

            ReDim Preserve mastrDetailLines(mintLineX)

            mastrDetailLines(mintLineX) = ("TOTALS FOR " & UCase(MonthName(CInt(Strings.Right(strPrevMonth, 2)))) & ", " _

                                        & Strings.Left(strPrevMonth, 4) & ":").PadLeft(83) _

                                        & Space(1) _

                                        & FormatCurrency(decExtPriceMonth, 2).PadLeft(11) _

                                        & Space(1) _

                                        & FormatCurrency(decDiscAmtMonth, 2).PadLeft(11) _

                                        & Space(1) _

                                        & FormatCurrency(decSaleTotMonth, 2).PadLeft(11)

            mintLineX += 1

            ReDim Preserve mastrDetailLines(mintLineX)

            mastrDetailLines(mintLineX) = " "

 

            ' "roll" the month totals to the grand totals

            decExtPriceGrand += decExtPriceMonth

            decDiscAmtGrand += decDiscAmtMonth

            decSaleTotGrand += decSaleTotMonth

 

        Loop

 

        ' end of outer processing loop - time to generate the grand totals

 

        mintLineX += 1

        ReDim Preserve mastrDetailLines(mintLineX)

        mastrDetailLines(mintLineX) = Space(70) _

                                    & "GRAND TOTALS:" _

                                    & Space(1) _

                                    & FormatCurrency(decExtPriceGrand, 2).PadLeft(11) _

                                    & Space(1) _

                                    & FormatCurrency(decDiscAmtGrand, 2).PadLeft(11) _

                                    & Space(1) _

                                    & FormatCurrency(decSaleTotGrand, 2).PadLeft(11)

 

        ' close the input file

        FileClose(mintSalesFileNbr)

 

    End Sub

 

In examining the code above, the variable strPrevMonth will be needed to compare the current contents of the year-month field with its previous contents to detect when a change in that field occurs. Also, because we will now have an additional set of totals at the year-month level for the calculated fields extended price, discount amount, and sale total, we declare the variables decExtPriceMonth, decDiscAmtMonth, and decSaleTotMonth for that purpose.

 

After the start of the main processing loop (after the line Do Until mblnEOF), there are several "setup" steps are coded to provide for the year/month control break. The statement

     

      strPrevMonth = mstrYearMonth

 

is critical because saves the value of the current year-month field (mstrYearMonth) to the "comparison" variable strPrevMonth.

 

To deal with printing the "month, year" sub-heading, some nested functions are used to convert what might be "200410" to "OCTOBER, 2004". If we break down UCase(MonthName(CInt(Strings.Right(mstrYearMonth, 2)))), the segment Strings.Right(mstrYearMonth, 2) would give you the string "10", the CInt function then converts that to an integer data type, the MonthName function would then return "October", then the UCase function would convert that to "OCTOBER". All that is concatenated with a comma followed by Strings.Left(mstrYearMonth, 4) which would give you "2004".

The statements

 

        decExtPriceMonth = 0

        decDiscAmtMonth = 0

        decSaleTotMonth = 0

 

are critical to producing accurate subtotals at the year-month level. These variables must be cleared at the start of every year-month group.

 

The statement

 

        Do Until (strPrevMonth <> mstrYearMonth) _

              Or (mblnEOF)

 

sets up an inner, or nested loop; it is the body of this inner loop which will process the detail records. The loop condition "Until (strPrevMonth <> mstrYearMonth)" means that the loop will keep executing until there is a change (or break) in the mstrYearMonth field – the condition checks the value of strPrevMonth (which we have set in the setup steps above) against the value of mstrYearMonth, which is refreshed every time an input record is read. This loop must also check for the end-of-file condition (hence the piece Or (mblnEOF)), and of course must terminate if that condition occurs.

 

The statements

 

            decExtPriceMonth += decExtPriceDtl

            decDiscAmtMonth += decDiscAmtDtl

            decSaleTotMonth += decSaleTotDtl

 

add the calculated fields at the detail level to their corresponding year-month totals. In Sample Program 1, the calculated fields at the detail level were added directly to the corresponding grand totals – but in Sample Program 2, there is the additional year-month subtotal level, and it is these year-month subtotal accumulators to which the detail variables must be added. The rule in a control-break subtotal program is that detail-level values or subtotal values are added (or "rolled") to the next highest level of totals.

 

The next set of new statements handle the control break condition – i.e., the code does what needs to be done when a change in the year-month is detected (which is basically to print the year-month subtotals and roll the year-month subtotals to the grand totals):

 

Once we move past the

 

        Loop

 

statement, this means a change has occurred in the value of the year-month field. We have printed all the detail records for one month, and we have the first record of the next month in memory waiting to be processed. However, before we start processing the new month, we have some unfinished business to take care of with the previous month. First, we must generate a subtotal line for the previous month; then the year/month totals are added to their corresponding grand totals. (As mentioned earlier, in a control-break subtotal program,  subtotal values are added (or "rolled") to the next highest level of totals.)

 

            decExtPriceGrand += decExtPriceMonth

            decDiscAmtGrand += decDiscAmtMonth

            decSaleTotGrand += decSaleTotMonth

 

From that point on, the code for this program is the same as its predecessor. (The Loop statement returns the program to the top of the main outer processing loop, which will do the setup for the next year/month group, process the detail records for that group, generate the group's subtotals, roll those subtotals to the grand totals, and so on for each group.) But when the end of file condition occurs, the grand totals are printed, the wrap-up code executes, and the report is done.

 

 

In the third sample program (which does control breaks by month and date in addition to generating grand totals), the GenerateDetailLineArray is coded as follows:

 

    Private Sub GenerateDetailLineArray()

 

        Dim strPrevMonth As String

        Dim strPrevDate As String

 

        Dim decExtPriceDtl As Decimal

        Dim decDiscAmtDtl As Decimal

        Dim decSaleTotDtl As Decimal

 

        Dim decExtPriceDate As Decimal

        Dim decDiscAmtDate As Decimal

        Dim decSaleTotDate As Decimal

 

        Dim decExtPriceMonth As Decimal

        Dim decDiscAmtMonth As Decimal

        Dim decSaleTotMonth As Decimal

 

        Dim decExtPriceGrand As Decimal

        Dim decDiscAmtGrand As Decimal

        Dim decSaleTotGrand As Decimal

 

        mintLineX = -1

        Erase mastrDetailLines

        mblnEOF = False

 

        ' open the customer data file ...

        mintSalesFileNbr = FreeFile()

        FileOpen(mintSalesFileNbr, mstrSalesFileName, OpenMode.Input)

        ' priming read

        Call ReadSalesRecord()

 

        ' main outer processing loop

        Do Until mblnEOF

 

            ' set up for month break

 

            strPrevMonth = mstrYearMonth

 

            mintLineX += 1

            ReDim Preserve mastrDetailLines(mintLineX)

            mastrDetailLines(mintLineX) = UCase(MonthName(CInt(Strings.Right(mstrYearMonth, 2)))) & ", " _

                                        & Strings.Left(mstrYearMonth, 4)

            mintLineX += 1

            ReDim Preserve mastrDetailLines(mintLineX)

            mastrDetailLines(mintLineX) = " "

 

            decExtPriceMonth = 0

            decDiscAmtMonth = 0

            decSaleTotMonth = 0

 

            ' inner loop 1 for month break

            Do Until (strPrevMonth <> mstrYearMonth) _

                  Or (mblnEOF)

 

                ' set up for date break

 

                strPrevDate = mstrOrderDate

 

                decExtPriceDate = 0

                decDiscAmtDate = 0

                decSaleTotDate = 0

 

                ' inner loop 2 for date break

                Do Until (strPrevDate <> mstrOrderDate) _

                      Or (strPrevMonth <> mstrYearMonth) _

                      Or (mblnEOF)

 

                    decExtPriceDtl = msngUnitPrice * mintQty

                    decDiscAmtDtl = decExtPriceDtl * msngDiscountPct

                    decSaleTotDtl = decExtPriceDtl - decDiscAmtDtl

 

                    ' generate a detail line

                    mintLineX += 1

                    ReDim Preserve mastrDetailLines(mintLineX)

                    mastrDetailLines(mintLineX) = Space(10) _

                                                & mstrOrderDate.PadRight(10) _

                                                & Space(1) _

                                                & mstrOrderNbr.PadRight(5) _

                                                & Space(2) _

                                                & mstrCustID.PadRight(5) _

                                                & Space(3) _

                                                & mstrProdDesc.PadRight(33) _

                                                & Space(1) _

                                                & FormatNumber(msngUnitPrice, 2).PadLeft(6) _

                                                & Space(3) _

                                                & mintQty.ToString.PadLeft(3) _

                                                & Space(2) _

                                                & FormatCurrency(decExtPriceDtl, 2).PadLeft(11) _

                                                & Space(1) _

                                                & FormatCurrency(decDiscAmtDtl, 2).PadLeft(11) _

                                                & Space(1) _

                                                & FormatCurrency(decSaleTotDtl, 2).PadLeft(11)

 

                    ' "roll" the detail amounts to the date totals

                    decExtPriceDate += decExtPriceDtl

                    decDiscAmtDate += decDiscAmtDtl

                    decSaleTotDate += decSaleTotDtl

 

                    ' read the next record

                    Call ReadSalesRecord()

 

                Loop

 

                ' end of inner loop 2

                ' we have our break on date, so print the date totals

 

                mintLineX += 1

                ReDim Preserve mastrDetailLines(mintLineX)

                mastrDetailLines(mintLineX) = Space(84) _

                                            & " ----------" _

                                            & Space(1) _

                                            & " ----------" _

                                            & Space(1) _

                                            & " ----------"

 

                mintLineX += 1

                ReDim Preserve mastrDetailLines(mintLineX)

                mastrDetailLines(mintLineX) = ("TOTALS FOR " & strPrevDate & ":").PadLeft(83) _

                                            & Space(1) _

                                            & FormatCurrency(decExtPriceDate, 2).PadLeft(11) _

                                            & Space(1) _

                                            & FormatCurrency(decDiscAmtDate, 2).PadLeft(11) _

                                            & Space(1) _

                                            & FormatCurrency(decSaleTotDate, 2).PadLeft(11)

                mintLineX += 1

                ReDim Preserve mastrDetailLines(mintLineX)

                mastrDetailLines(mintLineX) = " "

 

                ' "roll" the date totals to the month totals

                decExtPriceMonth += decExtPriceDate

                decDiscAmtMonth += decDiscAmtDate

                decSaleTotMonth += decSaleTotDate

 

            Loop

 

            ' end of inner loop 1

            ' we have our break on month, so generate the month totals

 

            mintLineX += 1

            ReDim Preserve mastrDetailLines(mintLineX)

            mastrDetailLines(mintLineX) = ("TOTALS FOR " & UCase(MonthName(CInt(Strings.Right(strPrevMonth, 2)))) & ", " _

                                        & Strings.Left(strPrevMonth, 4) & ":").PadLeft(83) _

                                        & Space(1) _

                                        & FormatCurrency(decExtPriceMonth, 2).PadLeft(11) _

                                        & Space(1) _

                                        & FormatCurrency(decDiscAmtMonth, 2).PadLeft(11) _

                                        & Space(1) _

                                        & FormatCurrency(decSaleTotMonth, 2).PadLeft(11)

            mintLineX += 1

            ReDim Preserve mastrDetailLines(mintLineX)

            mastrDetailLines(mintLineX) = " "

 

            ' "roll" the month totals to the grand totals

            decExtPriceGrand += decExtPriceMonth

            decDiscAmtGrand += decDiscAmtMonth

            decSaleTotGrand += decSaleTotMonth

 

        Loop

 

        ' end of outer processing loop - time to generate the grand totals

 

        mintLineX += 1

        ReDim Preserve mastrDetailLines(mintLineX)

        mastrDetailLines(mintLineX) = Space(70) _

                                    & "GRAND TOTALS:" _

                                    & Space(1) _

                                    & FormatCurrency(decExtPriceGrand, 2).PadLeft(11) _

                                    & Space(1) _

                                    & FormatCurrency(decDiscAmtGrand, 2).PadLeft(11) _

                                    & Space(1) _

                                    & FormatCurrency(decSaleTotGrand, 2).PadLeft(11)

 

        ' close the input file

        FileClose(mintSalesFileNbr)

 

    End Sub

 

Notes on Sample Program 3

 

Sample Program 3 builds on Sample Program 2. In addition to the year-month control break implemented in Sample Program 2, Sample Program 3 implements an additional control break on the order date. The report now includes subtotals by order date in addition to subtotals by year-month.

 

In the code above, the variable strPrevDate will be needed to compare the current contents of the order date field with its previous contents to detect when a change in that field occurs. Also, because we will now have an additional set of totals at the date level for the calculated fields extended price, discount amount, and sale total, we declare the variables decExtPriceDate, decDiscAmtDate, and decSaleTotDate for that purpose.

 

After the Do Until loop is set up for the year/month break (Do Until (strPrevMonth <>  mstrYearMonth) Or (mblnEOF)), we perform the setup steps for the control break on the order date field.

 

The statement

 

            strPrevDate = mstrOrderDate

 

performs the critical step of saving the value of the current date field (mstrOrderDate) to the "comparison" variable strPrevDate.

 

 

The statements

           

            decExtPriceDate = 0

            decDiscAmtDate = 0

            decSaleTotDate = 0

 

perform the critical step of clearing the date subtotals, which must be cleared at the start of every date group.

 

              

The statement

 

            Do Until (strPrevDate <> mstrOrderDate) _

                  Or (strPrevMonth <> mstrYearMonth) _

                  Or (mblnEOF)

 

sets up a second inner, or nested loop; it is the body of this inner loop which will process the detail records. The loop condition "Until (strPrevDate <> mstrOrderDate)" means that the loop will keep executing until there is a change (or break) in the mstrOrderDate field – the condition checks the value of strPrevDate (which we have set in the setup steps above) against the value of mstrOrderDate, which is refreshed every time an input record is read. Note also that we are checking for a change in the year/month field (hence the piece Or (strPrevMonth <> mstrYearMonth)). In a control-break program, a break in a higher level automatically causes a break in all lower levels. This loop must also check for the end-of-file condition (hence the piece Or (mblnEOF)), and of course must terminate if that condition occurs. It should be noted that the end-of-file condition is the "mother" of all control breaks – end-of-file automatically causes a break in all control levels.

 

 

The statements

 

                decExtPriceDate += decExtPriceDtl

                decDiscAmtDate += decDiscAmtDtl

                decSaleTotDate += decSaleTotDtl

 

add the calculated fields at the detail level to their corresponding date  totals. In Sample Program 1, the calculated fields at the detail level were added directly to the corresponding grand totals; in Sample Program 2, the calculated fields at the detail level were added to their corresponding year-month totals; now in Sample Program 3, there is the additional date subtotal level, and it is these date subtotal accumulators to which the detail variables must be added. As mentioned earlier, the rule in a control-break subtotal program is that detail-level values or subtotal values are added (or "rolled") to the next highest level of totals.

 

The next set of new statements handle the control break condition – i.e., the code does what needs to be done when a change in the order date is detected (which is basically to print the date subtotals and roll the date subtotals to the year-month subtotals):

 

Once we move past the

 

        Loop

 

statement, this means a change has occurred in the value of the order date field. We have printed all the detail records for one date, and we have the first record for the next date in memory waiting to be processed. However, before we start processing the new date, we have some unfinished business to take care of with the previous date. First, we must generate the subtotal line for the previous date. Then, we must add the date subtotals to their corresponding year/month subtotals. (As mentioned earlier, in a control-break subtotal program,  subtotal values are added (or "rolled") to the next highest level of totals.)

 

            decExtPriceMonth += decExtPriceDate

            decDiscAmtMonth += decDiscAmtDate

            decSaleTotMonth += decSaleTotDate

 

From that point on, the code for this program is very similar to its precedessor. (The Loop statement returns the program to the top of the first inner processing loop, which will do the setup for the next date group, process the detail records for that group, generate the date group's subtotals, roll those subtotals to the year/month subtotals, and so on for each date group. When there is a break on year/month, control will return to the main outer processing loop, which will do the setup for the next year/month group, process the date groups for that year/month group, generate the year/month group's subtotals, roll those year/month subtotals to the grand totals, and so on for each year/month group.) But when the end of file condition occurs, the grand totals are printed, the wrap-up code executes, and the report is done.

 

Notes on Sample Program 4

 

In the fourth sample program (which does control breaks by month, date, and order number in addition to generating grand totals), the GenerateDetailLineArray is coded as follows:

 

    Private Sub GenerateDetailLineArray()

 

        Dim strPrevMonth As String

        Dim strPrevDate As String

        Dim strPrevOrderNbr As String

 

        Dim decExtPriceDtl As Decimal

        Dim decDiscAmtDtl As Decimal

        Dim decSaleTotDtl As Decimal

 

        Dim decExtPriceOrd As Decimal

        Dim decDiscAmtOrd As Decimal

        Dim decSaleTotOrd As Decimal

 

        Dim decExtPriceDate As Decimal

        Dim decDiscAmtDate As Decimal

        Dim decSaleTotDate As Decimal

 

        Dim decExtPriceMonth As Decimal

        Dim decDiscAmtMonth As Decimal

        Dim decSaleTotMonth As Decimal

 

        Dim decExtPriceGrand As Decimal

        Dim decDiscAmtGrand As Decimal

        Dim decSaleTotGrand As Decimal

 

        mintLineX = -1

        Erase mastrDetailLines

        mblnEOF = False

 

        ' open the customer data file ...

        mintSalesFileNbr = FreeFile()

        FileOpen(mintSalesFileNbr, mstrSalesFileName, OpenMode.Input)

        ' priming read

        Call ReadSalesRecord()

 

        ' main outer processing loop

        Do Until mblnEOF

 

            ' set up for month break

 

            strPrevMonth = mstrYearMonth

 

            mintLineX += 1

            ReDim Preserve mastrDetailLines(mintLineX)

            mastrDetailLines(mintLineX) = UCase(MonthName(CInt(Strings.Right(mstrYearMonth, 2)))) & ", " _

                                        & Strings.Left(mstrYearMonth, 4)

            mintLineX += 1

            ReDim Preserve mastrDetailLines(mintLineX)

            mastrDetailLines(mintLineX) = " "

 

            decExtPriceMonth = 0

            decDiscAmtMonth = 0

            decSaleTotMonth = 0

 

            ' inner loop 1 for month break

            Do Until (strPrevMonth <> mstrYearMonth) _

                  Or (mblnEOF)

 

                ' set up for date break

 

                strPrevDate = mstrOrderDate

 

                decExtPriceDate = 0

                decDiscAmtDate = 0

                decSaleTotDate = 0

 

                ' inner loop 2 for date break

                Do Until (strPrevDate <> mstrOrderDate) _

                      Or (strPrevMonth <> mstrYearMonth) _

                      Or (mblnEOF)

 

                    ' set up for order break

 

                    strPrevOrderNbr = mstrOrderNbr

 

                    decExtPriceOrd = 0

                    decDiscAmtOrd = 0

                    decSaleTotOrd = 0

 

                    ' inner loop 3 for order number break

                    Do Until (strPrevOrderNbr <> mstrOrderNbr) _

                          Or (strPrevDate <> mstrOrderDate) _

                          Or (strPrevMonth <> mstrYearMonth) _

                          Or (mblnEOF)

 

                        decExtPriceDtl = msngUnitPrice * mintQty

                        decDiscAmtDtl = decExtPriceDtl * msngDiscountPct

                        decSaleTotDtl = decExtPriceDtl - decDiscAmtDtl

 

                        ' generate a detail line

                        mintLineX += 1

                        ReDim Preserve mastrDetailLines(mintLineX)

                        mastrDetailLines(mintLineX) = Space(10) _

                                                    & mstrOrderDate.PadRight(10) _

                                                    & Space(1) _

                                                    & mstrOrderNbr.PadRight(5) _

                                                    & Space(2) _

                                                    & mstrCustID.PadRight(5) _

                                                    & Space(3) _

                                                    & mstrProdDesc.PadRight(33) _

                                                    & Space(1) _

                                                    & FormatNumber(msngUnitPrice, 2).PadLeft(6) _

                                                    & Space(3) _

                                                    & mintQty.ToString.PadLeft(3) _

                                                    & Space(2) _

                                                    & FormatCurrency(decExtPriceDtl, 2).PadLeft(11) _

                                                    & Space(1) _

                                                    & FormatCurrency(decDiscAmtDtl, 2).PadLeft(11) _

                                                    & Space(1) _

                                                    & FormatCurrency(decSaleTotDtl, 2).PadLeft(11)

 

                        ' "roll" the detail amounts to the order totals

                        decExtPriceOrd += decExtPriceDtl

                        decDiscAmtOrd += decDiscAmtDtl

                        decSaleTotOrd += decSaleTotDtl

 

                        ' read the next record

                        Call ReadSalesRecord()

 

                    Loop

 

                    ' end of inner loop 3

                    ' we have our break on order number, so generate the order totals

 

                    mintLineX += 1

                    ReDim Preserve mastrDetailLines(mintLineX)

                    mastrDetailLines(mintLineX) = Space(84) _

                                                & "-----------" _

                                                & Space(1) _

                                                & "-----------" _

                                                & Space(1) _

                                                & "-----------"

                    mintLineX += 1

                    ReDim Preserve mastrDetailLines(mintLineX)

                    mastrDetailLines(mintLineX) = Space(53) _

                                                & "TOTALS FOR ORDER NUMBER " & strPrevOrderNbr & ":" _

                                                & Space(1) _

                                                & FormatCurrency(decExtPriceOrd, 2).PadLeft(11) _

                                                & Space(1) _

                                                & FormatCurrency(decDiscAmtOrd, 2).PadLeft(11) _

                                                & Space(1) _

                                                & FormatCurrency(decSaleTotOrd, 2).PadLeft(11)

                    mintLineX += 1

                    ReDim Preserve mastrDetailLines(mintLineX)

                    mastrDetailLines(mintLineX) = " "

 

                    ' "roll" the order totals to the date totals

                    decExtPriceDate += decExtPriceOrd

                    decDiscAmtDate += decDiscAmtOrd

                    decSaleTotDate += decSaleTotOrd

 

                Loop

 

                ' end of inner loop 2

                ' we have our break on date, so print the date totals

 

                mintLineX += 1

                ReDim Preserve mastrDetailLines(mintLineX)

                mastrDetailLines(mintLineX) = ("TOTALS FOR " & strPrevDate & ":").PadLeft(83) _

                                            & Space(1) _

                                            & FormatCurrency(decExtPriceDate, 2).PadLeft(11) _

                                            & Space(1) _

                                            & FormatCurrency(decDiscAmtDate, 2).PadLeft(11) _

                                            & Space(1) _

                                            & FormatCurrency(decSaleTotDate, 2).PadLeft(11)

                mintLineX += 1

                ReDim Preserve mastrDetailLines(mintLineX)

                mastrDetailLines(mintLineX) = " "

 

                ' "roll" the date totals to the month totals

                decExtPriceMonth += decExtPriceDate

                decDiscAmtMonth += decDiscAmtDate

                decSaleTotMonth += decSaleTotDate

 

            Loop

 

            ' end of inner loop 1

            ' we have our break on month, so generate the month totals

 

            mintLineX += 1

            ReDim Preserve mastrDetailLines(mintLineX)

            mastrDetailLines(mintLineX) = ("TOTALS FOR " & UCase(MonthName(CInt(Strings.Right(strPrevMonth, 2)))) & ", " _

                                        & Strings.Left(strPrevMonth, 4) & ":").PadLeft(83) _

                                        & Space(1) _

                                        & FormatCurrency(decExtPriceMonth, 2).PadLeft(11) _

                                        & Space(1) _

                                        & FormatCurrency(decDiscAmtMonth, 2).PadLeft(11) _

                                        & Space(1) _

                                        & FormatCurrency(decSaleTotMonth, 2).PadLeft(11)

            mintLineX += 1

            ReDim Preserve mastrDetailLines(mintLineX)

            mastrDetailLines(mintLineX) = " "

 

            ' "roll" the month totals to the grand totals

            decExtPriceGrand += decExtPriceMonth

            decDiscAmtGrand += decDiscAmtMonth

            decSaleTotGrand += decSaleTotMonth

 

        Loop

 

        ' end of outer processing loop - time to generate the grand totals

 

        mintLineX += 1

        ReDim Preserve mastrDetailLines(mintLineX)

        mastrDetailLines(mintLineX) = Space(70) _

                                    & "GRAND TOTALS:" _

                                    & Space(1) _

                                    & FormatCurrency(decExtPriceGrand, 2).PadLeft(11) _

                                    & Space(1) _

                                    & FormatCurrency(decDiscAmtGrand, 2).PadLeft(11) _

                                    & Space(1) _

                                    & FormatCurrency(decSaleTotGrand, 2).PadLeft(11)

 

        ' close the input file

        FileClose(mintSalesFileNbr)

 

    End Sub

 

Sample Program 4 builds on Sample Program 3. In addition to the year-month and order date control breaks implemented in Sample Program 3, Sample Program 4 implements an additional control break on order number. The report now includes subtotals by order number in addition to subtotals by year-month and order date. As in Sample Programs 1, 2, and 3, grand totals are printed in bold at the end of the report.

 

In the code above, the variable strPrevOrderNbr will be needed to compare the current contents of the order number field with its previous contents to detect when a change in that field occurs. Because we will now have an additional set of totals at the order number level for the calculated fields extended price, discount amount, and sale total, we declare the variables decExtPriceOrd, decDiscAmtOrd, and decSaleTotOrd for that purpose.

 

After the Do Until loop is set up for the date break (Do Until (strPrevDate <> mstrOrderDate) Or (strPrevMonth <>  mstrYearMonth) Or (mblnEOF)), we perform the setup steps for the control break on the order number field.

 

The statement

 

            strPrevOrderNbr = mstrOrderNbr

 

performs the critical step of saving the value of the current order number field (mstrOrderNbr) to the "comparison" variable strPrevOrderNbr.

 

 

The statements

           

            decExtPriceOrd = 0

            decDiscAmtOrd = 0

            decSaleTotOrd = 0

 

perform the critical step of clearing the order number subtotals, which must be cleared at the start of every order number group.

 

 

The statement

 

                Do Until (strPrevOrderNbr <> mstrOrderNbr) _

                      Or (strPrevDate <> mstrOrderDate) _

                      Or (strPrevMonth <> mstrYearMonth) _

                      Or (mblnEOF)

 

sets up a third inner, or nested loop; it is the body of this inner loop which will process the detail records.  The loop condition "Until (strPrevOrderNbr <> mstrOrderNbr)" means that the loop will keep executing until there is a change (or break) in the mstrOrderNbr field – the condition checks the value of strPrevOrderNbr (which we have set in the setup steps above) against the value of mstrOrderNbr, which is refreshed every time an input record is read. Note also that we are checking for a change in the date field (hence the piece Or (strPrevDate <> mstrOrderDate)) and we are checking for a change in the year/month field (hence the piece Or (strPrevMonth <> mstrYearMonth)). As mentioned several times in this article, in a control-break program, a break in a higher level automatically causes a break in all lower levels. This loop must also check for the end-of-file condition (hence the piece Or (mblnEOF)), and of course must terminate if that condition occurs. Recall that the end-of-file condition is the "mother" of all control breaks – end-of-file automatically causes a break in all control levels.

 

The statements

 

                    decExtPriceOrd += decExtPriceDtl

                    decDiscAmtOrd += decDiscAmtDtl

                    decSaleTotOrd += decSaleTotDtl

 

add the calculated fields at the detail level to their corresponding order number subtotals. In Sample Program 1, the calculated fields at the detail level were added directly to the corresponding grand totals; in Sample Program 2, the calculated fields at the detail level were added to their corresponding year-month totals; in Sample Program 3, the calculated fields at the detail level were added to their corresponding date totals; now in Sample Program 4, there is the additional order number subtotal level, and it is these order number subtotal accumulators to which the detail variables must be added. As mentioned earlier, the rule in a control-break subtotal program is that detail-level values or subtotal values are added (or "rolled") to the next highest level of totals.

 

The next set of new statements handle the control break condition – i.e., the code does what needs to be done when a change in the order number is detected (which is basically to print the order number subtotals and roll the order number subtotals to the date subtotals):

 

Once we move past the

 

        Loop

 

statement, this means a change has occurred in the value of the order number field. We have printed all the detail records for one order number, and we have the first record for the next order number in memory waiting to be processed. However, before we start processing the new order number, we have some unfinished business to take care of with the previous order number. First, we must print the subtotals for the previous order number. Then, we must add the order number subtotals to their corresponding date subtotals. (As mentioned earlier, in a control-break subtotal program,  subtotal values are added (or "rolled") to the next highest level of totals.)

 

                decExtPriceDate += decExtPriceOrd

                decDiscAmtDate += decDiscAmtOrd

                decSaleTotDate += decSaleTotOrd

 

From that point on, the code for this program is very similar to its precedessor. (The Loop statement returns the program to the top of the second inner processing loop, which will do the setup for the next order number group, process the detail records for that group, generate the order number group's subtotals, roll those subtotals to the date subtotals, and so on for each order number. When there is a break on date, control will return to the first  inner processing loop, which will do the setup for the next date group, process the order number groups for that date group, generate the date group's subtotals, and roll those subtotals to the year/month subtotals, and so on for each date group. When there is a break on year/month, control will return to the main outer processing loop, which will do the setup for the next year/month group, process the date groups for that year/month group, generate the year/month group's subtotals, roll those year/month subtotals to the grand totals, and so on for each year/month group.) But when the end of file condition occurs, the grand totals are printed, the wrap-up code executes, and the report is done.

 

Notes on Printing

 

The printing routines for all four sample programs are identical, with the exception of the heading line that specifies "WITH CONTROL BREAK ON MONTH", or "WITH CONTROL BREAKS ON MONTH, DATE, AND ORDER NUMBER". etc. In all cases, printing is triggered when the Document property of the PrintPreviewDialog is assigned the PrintDocument object. This causes the PrintPage event to fire, which does the work of populating the document with the contents of the detail line array that has been built previously by the program. Helper routines handle the generation and printing of page heading lines and also handles the case where a line is to be printed in bold font.

 

A Final Note (Regarding Sorting)

 

The first statement in this article begins "Control break processing is a technique used to process a sorted sequential file that contains groups of records ...". It must be emphasized that in order to process a file or set of data using the control break techniques discussed in this article, that file or set of data must be sorted on the key field or fields on which you are going to be breaking. For example, the comma-delimited file used in all of the examples in this article was pre-sorted on year-month, then date, then order number.

 

If you were to adapt the techniques learned in this article to processing a recordset that was the result of a database query, your query would specify the ORDER BY clause on the required fields. If you wanted to process a sequential file like the one used in the examples, and the file was not already sorted in the required sequence, you would have to look into ways of getting sorted prior to running your program. One option, if the file was in comma-delimited format, would be to import it into Excel, use Excel's Data Sort feature, then re-save the sorted results to a new file.

 

The bottom line is that control break processing requires that the input data is sorted on the control fields.

 

Downloads

 

Download the project files for Sample Program 1 here.

Download the project files for Sample Program 2 here.

Download the project files for Sample Program 3 here.

Download the project files for Sample Program 4 here.