Вы находитесь на странице: 1из 15

# Calculating the number of Business Days between two dates

Use the following formula, replacing the items in Bold with your start/end date and the items in Italics with your holidays........ WhilePrintingRecords; //Set the values of Start Date and End Date DateVar StartDate := Date(2003,01,01); DateVar EndDate := Date(2003,12,31); //Find out the difference in days and subtract the weekends NumberVar DaysDiff := DateDiff("d",StartDate,EndDate) DateDiff("ww",StartDate,EndDate,crsaturday) DateDiff("ww",StartDate,EndDate,crsunday); //Create an array of Holiday dates Local DateVar Array Holidays := MakeArray( Date(2003,01,01), Date(2003,01,02), Date(2003,12,25), Date(2003,12,26), Date(2003,07,07)); //Loop through the array checking if each holiday is within the dates Numbervar Counter := 0; While UBound(Holidays) <> Counter do (Counter := Counter + 1; if Not(dayofweek(Holidays[Counter]) in [1,7]) and Holidays[Counter] in StartDate to EndDate then DaysDiff := DaysDiff -1;); //Display result to 0 decimal places and no thousand separator totext(DaysDiff,0,"");

## Common formula examples

The current formulas include: 1. The number of "Work Days" between any two dates: 2. Converting a Numeric Date to True Date: 3. Converting a Character Date to True Date: 4. Use a parameter prompt to select the Sort/Group order, using fields of mixed data types: 5. Adding a month or months to a date: 6. Calculating an accurate Age Integer from a Date of Birth: 7. Printing Parameter Selections for Multiple or Range Values: 8. Making Multiple Variable Assignments in One Formula: 9. Converting total seconds to a formatted &quot;elapsed time&quot; string: 10. Conditional Total using an If-Then-Else formula: 11. Conditional Total using a Running Total: 12. Manual Cross-Tabs (Cross-Tabs that allow formulas): 13. The number of Business Hours between DateTime Values. 14. Distinct Count of an If-Then-Else Formula. 15. Convert Decimal Numbers to Text, showing only the Non-Zero Decimal Places. 16. Append a List of Detail Strings into one Line.

## 17. 18. 19. 20. 21.

Adding a Number of Business Days to a Date How to Print "Continued on Next Page" Finding the Last Friday of the Month Removing the Path from the "File Path and Name" field Find Monday of a specific week (using ISO standard week numbers)

## Crystal SQL and Date Parameters

If you've run into a problem with a SQL database and Date (or DateTime) selection criteria, you're not alone. It seems that the parameter values want to save with the Crystal SQL, and it just doesn't work properly. This is a known Crystal anomaly. A way to work around it is to cast your Date (or DateTime) parameter and database fields as strings (in formula fields), manipulate them so that they're in the format YYYYMMDD, and do your selection on the formula fields. Do this and Crystal ignores them for the purposes of its SQL, which may solve your problem. For example, say you have a Date-type parameter field called Date0. You can then create a formula called ParamDateStr like this: StringVar STrDOB := ToText({?Date0}); NumberVar NumSlash1 := InSTr(StrDOB, "/"); NumberVar NumSlash2 := InStr(NumSlash1+ 1, STrDOB, "/"); StringVar StrYr := mid(StrDOB, NumSlash2 + 1, 4); StringVar StrMo := left(STrDOB, NumSlash1 - 1); StringVar StrDay := mid(STrDOB, NumSlash1 + 1, NumSlash2 - NumSlash1 - 1); NumberVar NumMoLen := length(StrMo); if NumMoLen < 2 then StringVar StrMo := "0" + StrMo; StringVar StrDay := mid(STrDOB, NumSlash1 + 1, NumSlash2 - NumSlash1 - 1); if length(StrDay) = 1 then StringVar StrDay := "0" + StrDay; StrYr + StrMo + StrDay What you've done is to parse the Date, then rearrange the year, month and day values (making sure that months and days are two characters in length). Now, for your database field: StringVar STrDOB := ToText({Table.DateField}); NumberVar NumSlash1 := InSTr(StrDOB, "/"); NumberVar NumSlash2 := InStr(NumSlash1+ 1, STrDOB, "/"); StringVar StrYr := mid(StrDOB, NumSlash2 + 1, 4); StringVar StrMo := left(STrDOB, NumSlash1 - 1); NumberVar NumMoLen := length(StrMo); if NumMoLen < 2 then StringVar StrMo := "0" + StrMo; StringVar StrDay := mid(STrDOB, NumSlash1 + 1, NumSlash2 - NumSlash1 - 1); if length(StrDay) = 1 then StringVar StrDay := "0" + StrDay; StrYr + StrMo + StrDay What you have NOW is two strings, each created from a date, and each formatted

identically as YYYYMMDD. Finally, your selection criterion uses these two formula fields: {@DOBString} >= {@ParamDateStr} or whatever makes sense under your circumstances.

## Data Types in Crystal Reports

When asking a question in the forum that relates to formulas it is essential that you know the data types of all database fields involved. The basic 4 types are Number, Character, Date and Boolean. Each of these might have several subtypes like Currency, VarChar, DateTime, etc. The essential thing to uncover is the data type as seen by Crystal Reports. Keep in mind that this may not be the same as the data type specified in the database. To know what Crystal is seeing, place the field onto the canvas (design screen), right click on the field, and select &quot;Browse Field Data&quot;. This will show you sample values, and will also show you the data type as seen by Crystal Reports. DataType is not the same as Format. This is especially true for dates, which can be formatted in any number of ways. This, however, has no effect on the data type of the underlying field. So, when you ask a question about a formula, please include the data type of the fields involved in your post. If it is a character field include some sample values.

## Excluding null values when executing distinctcount

Created a formula called @Null with absolutely nothing in it and refer to it in your formula. It works! Don't know why but it does. FuncPass: if {v_Detail_Report.TestType}="Functional" and {v_Detail_Report.EX}="PP" then {v_Detail_Report.SerialNumber} else {@null}

How do I determine the correct accounting month based on '44-5' accounting periods
Recently, I was tasked to build a series of reports that were based on standard '4-45' accounting months (referred to as 'Operating Months' by my client and, therefore, in my formula). Since native Crystal Reports Date functions are based on calendar months, I had to come up with a formula to identify the specific accounting month for each record in my report. The first step in this process was to identify the rules by which a '4-4-5' accounting month is based. Following are the general rules: 1. The accounting year always starts on the first day of the month of the first month in the fiscal year

2. The Accounting Year always ends on the last day of the month in the last month of the fiscal year[li]Each quarter in the year is comprised of Accounting Months as follows:[ul] 3. Four weeks in the first month of the quarter o unless the first day of the fiscal year falls on the last day of the fiscal week- in this case the first fiscal month is comprised of four weeks plus 1 day 4. Four weeks in the second month of the quarter 5. Five weeks in the third month of the quarter o except for the last fiscal month since the last accounting month runs all the way to the end of the fiscal year, regardless of the number of weeks The next step was to identify the client-specific rules: 1. The fiscal year begins on January 1st 2. The fiscal year ends on December 31st 3. The fiscal week ends on Saturday (many companies end the week on Friday) Based on both the general rules and the client-specific rules listed above, I created the following formula*://@Operating MonthDateVar FirstDay;DateVar LastDay; DateVar FirstSat; StringVar YrText; StringVar OpMonth; //Defines the Date Value for the First Day of the Year FirstDay := Date(Year({TEST_TABLE.TEST_DATE}),1,1);//Substitute your fiscal Month and Day values if different //Defines the Date Value for the Last Day of the Year LastDay := Date(Year({TEST_TABLE.TEST_DATE}),12,31); //Substitute your fiscal Month and Day values if different //Defines the Date Value for the First Saturday of the Year - modify this formula if your fiscal week ends on Friday If DayOfWeek(FirstDay) < 7 Then FirstSat := FirstDay + (7 - DayOfWeek(FirstDay)) Else If DayOfWeek(FirstDay) = 7 Then FirstSat := FirstDay + 7;//Pushed out a week because the first day of the year was a Saturday //Defines the YrText Variable in 'YYYY' format YrText := Left(ToText(Year({TEST_TABLE.TEST_DATE}),0),1) + Right(ToText(Year({TEST_TABLE.TEST_DATE}),0),3); //Defines the Actual Operating Month for the Test Date //Modify the '/MM' values to your fiscal year OpMonth := Select {TEST_TABLE.TEST_DATE} Case FirstDay to (FirstSat + 21): YrText + '/01'//January 1 to the end of the 4th Saturday

Case (FirstSat + 22) to (FirstSat + 49): YrText + '/02'//28 Day Range (4 weeks, Sunday to Saturday) Case (FirstSat + 50) to (FirstSat + 84): YrText + '/03'//35 Day Range (5 weeks, Sunday to Saturday) Case (FirstSat + 85) to (FirstSat + 112): YrText + '/04'//28 Day Range (4 weeks, Sunday to Saturday) Case (FirstSat + 113) to (FirstSat + 140): YrText + '/05'//28 Day Range (4 weeks, Sunday to Saturday) Case (FirstSat + 141) to (FirstSat + 175): YrText + '/06'//35 Day Range (5 weeks, Sunday to Saturday) Case (FirstSat + 176) to (FirstSat + 203): YrText + '/07'//28 Day Range (4 weeks, Sunday to Saturday) Case (FirstSat + 204) to (FirstSat + 231): YrText + '/08'//28 Day Range (4 weeks, Sunday to Saturday) Case (FirstSat + 232) to (FirstSat + 266): YrText + '/09'//35 Day Range (5 weeks, Sunday to Saturday) Case (FirstSat + 267) to (FirstSat + 294): YrText + '/10'//28 Day Range (4 weeks, Sunday to Saturday) Case (FirstSat + 295) to (FirstSat + 322): YrText + '/11'//28 Day Range (4 weeks, Sunday to Saturday) Case (FirstSat + 323) to LastDay: YrText + '/12'//Beginning of the Operating month to December 31st; OpMonth;//This is the return value for the Operating Month in 'YYYY/MM' format for easy sorting and grouping *Due to the Case logic, this formula will only work in Crystal Reports 8.0 and above. In order to work in earlier versions, you will need to substitute If-Then-Else statements as follows: If {TEST_TABLE.TEST_DATE} in FirstDay to (FirstSat + 21) Then OpMonth := YrText + '/01'//January 1 to the end of the 4th Saturday Else If//etc I think you'll find that the above formula works very well. For my client, the database field date (referenced as {TEST_TABLE.TEST_DATE}) '01/26/2002' would return '2002/01' as the Operating Month, but '01/27/2002' would return '2002/02'. These are the correct Operating Months, based on my client's '4-4-5' accounting periods.

How do I subtract one datetime field from another and display days, hours, minutes and seconds?
When you subtract one datetime field from another datetime field in Crystal the result is the number of days and the decimal fraction of day. Assume the following dates: D1 := Datetime(2001,10,24,09,00,00) // 9:00 AM, October 24, 2001 D2 := Datetime(2001,10,24,08,59,59) // 8:59 AM, October 24, 2001 D3 := Datetime(2001,01,01,09,00,00) // 9:00 AM, January 1, 2001 D1 - D2 = .0000115741 There is a one-second difference between D1 and D2. There are 86,400 seconds in a

day. Dividing 1 by 86,400 yields .0000115741 D1 - D3 = 296.00000 There are exactly 296 days between D1 and D3. Since there are no hours, minutes or seconds between the two datetimes, the decimal portion of the difference is zero. D2 - D3 = 295.9999884259 There are 295 days, 23 hours, 59 minutes and 59 seconds between D2 and D3. 86400 seconds times .9999884259 is 86399 or one second less than a day. Let us suppose that our client required a report that showed the difference between D2 and D3 in days, hours, minutes and seconds formatted in a sentence. For example: "There are 295 days, 23 hours, 59 minutes and 59 seconds between the two dates." One would think that this is easy with the new DateDiff function. The DateDiff function requires the following syntax: DateDiff(intervaltype,StartDateTime,EndDateTime) Where intervaltype is a string with a value such as: "d" for days "h" for hours "n" for minutes "s" for seconds However, the expression DateDiff("d",{@D2},{@D3}) using the examples above, yields 296 days, not the correct 295. As a result, we must use the following expression: numbervar tsecs := datediff("s",{@d3},{@d2}); // number of seconds between the dates numbervar ndays := truncate(tsecs/86400); // divide by the seconds in a day tsecs := remainder(tsecs,86400); // find the left over seconds numbervar nhours := truncate(tsecs/3600); // divide by the seconds in an hour tsecs := remainder(tsecs,3600); // find the left over seconds numbervar nmin := truncate(tsecs/60); // divide by the seconds in a minute tsecs := remainder(tsecs,60); // find the left over seconds // now that we have all the components, we put it together in a sentence "There are "+totext(ndays,0)+" days, "+totext(nhours,0)+" hours, "+ totext(nmin,0)+

" minutes, "+totext(tsecs,0)+" seconds, between the two dates." The remainder function divides the second argument into the first and returns the remainder after the division. The Totext function changes numeric values into string values and must be used when appending numbers to text in this way. The zero argument indicates that we want no decimals.

## What is a SQL Expression?

//Record Selection Criteria - Direct Method 1 {Table.Date_Field} In Date(Right({?From_Date},4), Left({?From_Date},2), Mid({? From_Date},4,2)) to Date(Right({?To_Date},4), Left({?To_Date},2), Mid({? To_Date},4,2)) //Record Selection Criteria - Direct Method 2 ToText({Table.Date_Field},'MM/dd/yyyy') In {?From_Date} to {?To_Date} //Record Selection Criteria - Indirect Method {@Date} In {?From_Date} to {?To_Date} Where @Date is a formula defined as follows: //@Date ToText(ToText({Table.Date_Field},'MM/dd/yyyy')) Whichever method you choose, your Record Selection Criteria won't be evaluated until all records have been returned. Depending both on the size and integrity of your database and your machine's processor, RAM and Hard Drive space, you could be in for a very long wait. Please note that the following SQL Statement (based on the last example) does not contain a Where Clause: SELECT "TABLE"."DATE_FIELD" FROM "DATABASE"."TABLE" "TABLE_NAME" If you were to create a SQL Expression as follows (Oracle 8i Native Driver in this example): //%Date TO_CHAR(TO_CHAR("TABLE"."DATE_FIELD",'MM/DD/YYYY') ,'MM/DD/YYYY') Then you could use it in your Record Selection Statement as follows: //Record Selection using a SQL Expression {%Date} In {?From_Date} to {?To_Date} In this example, the Record Selection Criteria is passed to the Database and is, therefore, processed on the Server. This could result in a very significant performance increase. Please note that the Customized Parameter Selection is passed to the Database in the Where Clause of the following SQL Statement: SELECT TO_CHAR("TABLE"."DATE_FIELD",'MM/DD/YYYY') FROM "DATABASE"."TABLE" "TABLE_NAME" WHERE TO_CHAR("TABLE"."DATE_FIELD",'MM/DD/YYYY') >= '11/01/2001' AND TO_CHAR("TABLE"."DATE_FIELD",'MM/DD/YYYY') <= '11/05/2001' Q: When is it appropriate to use SQL Expressions?

A: Unfortunately, the answer to this question is a little more nebulous as it largely depends on a combination of your skills, database permissions and your Report Development environment. For example, many companies have defined barriers between Report Writers and Database Developers. In these cases, the Report writer may not have permission to create objects such as Views, Stored Procs or Functions on the Database. As such, the Report Writer must build reports using only the existing database objects. In this type of scenario, SQL Expressions can be very effective since you are basically building a subquery or Function (User Defined Function in SQL Server) outside of the actual database environment. That is, you have the ability to create customized fields that are handled as if they were actual database fields. If you, as a Report Writer or Developer, have the skills to create database objects and have the necessary level of database permissions and are in a development environment that supports the addition of new database objects then you probably don't need to use SQL Expressions since all of your database field manipulation can be done in the database object you create. Generally speaking, however, if you can substitute a SQL Expression for an equivalent Formula (based on your available SQL Functions) then it is appropriate to do so. As explained above, this is especially important when it comes to your Record Selection Criteria.

## Working with rolling dates

Often we encounter reporting requirements where we need to prompt the user for a starting or ending date and then display columns of information for a number of previous periods. I encountered this when working with a health care organization in Pennsylvania. They wanted to show revenue by month for the previous 12 months based on a date provided by a parameter field. I could not use Crystal's cross tabulation expert because many of the other calculations and accumulations were just too complex. I used Crystal array capability to solve the problem. The following formula simply provides the correct column heading for the second column in the report. The second column could be any of the twelve months depending on the value of the parameter field. //labmon2 stringVar array Months := ["Jan","Feb","Mar","Apr","May","Jun", ,"Nov","Dec"]; numberVar mon := Month({?High Date})-1; numberVar monF := IF mon <1 then 12+ mon; numberVar yearF := IF mon <1 then Year({?High Date}) -1 else Year({?High Date}); Months[monF]+" "+ToText (yearF,0,"") "Jul","Aug","Sep","Oct"

In the first line, I create a 12-position array of the month abbreviations. In the second line I create a memory variable, mon, which is equal to the month

number of the parameter field's date minus 1. I am subtracting 1 because it is in the second column. For column 3 I would subtract 2, etc. In the third line I test to see if I have crossed the boundary between years. If High Date were equal to January, mon would be equal to 0. In that case monF would be equal to 12 + 0 = 12 or December. In the fourth line I do the same to find the correct year. The fifth line puts it all together as a text string. I used the same logic to calculate the proper amounts for each group footer line. Note that this will work for any date.

## Displaying a Random Sample (number or proportion)

If you need to randomly select and display a given number of records, or a userspecified proportion of the records, here's a simple approach: Step 1: Place the following formula (@Random_Number) in the detail section: --------------Rnd() --------------Step 2: Sort the records by @Random_Number (ascending). Step 3: In the suppress attribute of the detail section enter the following expression: 3.1 If you need to display a given number of Records: ---------------------------------------RecordNumber > {?Show_N_Records} ---------------------------------------where {?Show_N_Records} is a parameter allowing the user to specify the number of records to be shown. 3.2 If you need to display a given proportion of Records: ---------------------------------------RecordNumber > ({?Show_X_Percent}/100) * Count({some_field}) ---------------------------------------where {?Show_X_Percent} is a parameter allowing the user to specify a number between 0 and 100

Generating Reports using Stored Procedures for Visual Basic Front Ends
I have been working on this problem for a while now. I have finally found out how to make it work. The problem with Crystal Reports is that it cannot generate a report with a recordcount of -1. How is it possible to have a recordcount of -1 you might ask? When using a recordset, you can choose what cursor type you would like. The options are Dynamic, ForwardOnly, Keyset, and Static. Each of these cursor types are good for certain things but I'd like to talk about the ForwardOnly cursor type. ForwardOnly cursor types are fast, very good for retrieving data, However as the name might suggest, The cursor is 'forward only', so you can't navigate very well with this cursortype, you can only go forward. This is the reason that you receive -1 as a recordcount. To get a recordcount the cursor has to run all the way through the recordset to get the count and then go back to the top so you can navigate the recordset, but with a ForwardOnly cursor you cant do that. There is a way to get around this however. The recordset Object's cursor location is Server Side by default. This is a good thing because you typically want the server to do all the work. But if you change the cursor type from Server Side to Client Side, you can get a record count from a forward only RecordSet. I can't exactly explain why this is so, other than I think a recordset generated on a Server will not return the recordcount, but if you are generating the recordset on your own computer, it is easier to calculate a recordcount. So what does this have to do with Stored Procedures? Well, Stored Procedures only return ForwardOnly recordsets. So if you want to generate a report using a Stored Procedure, the cursor type needs to be Client Side. I hope this helps anyone who needs it.

## How do Duplicate Records based on a Quantity Field?

Problem: A typical situation is where a number of label copies must be printed for each order according to the {quantity} field in each order record. Solution: Create a REPEATER table with a single column (How_Many) that looks like this: How_Many 1 2 3 4 5 6 etc. Now, in your report, add the Repeater Table and add a join condition of:
CODE

## Order.quantity >= Repeater.How_Many

If the &quot;>=&quot; join option is not available, remove the join to the REPEATER table and create this condition in the Record Selection condition:
CODE

{Order.quantity} >= {Repeater.How_Many} If you are restricted to using an equal join, you can also modify the repeater table to include N records for each quantity N: 1 2 2 3 3 3 ... This would cause each order to be duplicated as many times as the value of {Order.quantity}.

## How do I improve report processing performance?

performance, as hiding a section which will calculate and save drill-down information takes longer than a section that is suppressed (no drill-down). h.)Make sure that Use Indexes for Speed is checked ON under File|Report Options, other-wise PC database reports will not use indexes to retrieve information, and SQL database reports will return the entire dataset before Crystal filters it on the local machine. i.)Creating a stored procedure or parameterized stored procedure, and reporting off that, is by far the quickest way to retrieve a dataset from the server. Database Dependent Issues Specific to PC Databases: a.)If you are accessing PC Databases, always try to use native database drivers to access your data rather than ODBC. ODBC will always perform slower than native access, as information must be "translated" by the ODBC driver before being sent to your database for retrieval, and then again on the way back, the returned information must pass through the ODBC driver to be translated into your report. Specific to SQL Databases: a.)With SQL databases, both ODBC and Native drivers that ship with Crystal 5.0 are comparable In performance. b.)If you modify the Database|Show SQL Query statement in Crystal, any selection criteria you add later to the report will be processed locally on the client side. You must make sure to reset the query first, and then add a selection criteria to the report, which in turn will then be appended to the existing SQL query so that processing occurs on the SERVER side. c.)Selections on Parameter fields WILL be sent off as part of the query that gets sent over to the server, with the exception of the Database|Show SQL Query statement being modified, in which case processing will occur locally. d.)Creating a report on a Query file (.QRY) and then adding a parameter field in the Selection Formula of the report will cause ONLY the query to be processed on the server side. The actual filtering of the parameter field selection will occur on the client side. e.)Do not use IFELSE conditions in your selection formula, as this also causes processing to occur on the local machine (for SQL databases).

How do I pass date parameters to sub reports and use them in the selection criteria?
. Introduction This document describes the steps that will allow nested sub reports to share some or all of the selection parameters of the parent report. This document shows reports that will share a date range as their parameters as this is a common link between reports. The instructions assume that both the Primary and Sub reports have been completed and that the sub report has been inserted into the footer of the Primary report. 2. Instructions 2.1. Set up the Primary Report In the primary report, create 2 parameter fields called startdate and enddate. You do not have to show these fields in the report.

These parameters are then shown in the report fields list as ?startdate and ? enddate. Then create 2 formulas called startdate and enddate. The startdate formula should only contain the report field value {?startdate} and the enddate formula should only contain the report field value {?enddate}. These Formulas are then shown in the report fields list as @startdate and @enddate. These formulas are required because you cannot create a subreport link based on a parameter field. You must then edit the selection criteria for the main report to include the data range. Go to the menu options Report|Edit Selection Formula|Record and enter/include the following record selection criteria: {table.datefield} >= {@startdate} and {table.datefield} <= {@enddate} 2.2. Set up the Sub Report Create the same 2 parameter fields and the same 2 formula fields in the subreport. These do not have to be on the actual report. 2.3. Link the Reports To link the reports go to the menu options Edit|Subreport Links. Select the formula @startdate from the available fields. This will activate field link section at the bottom of the screen. Remove the tick from the checkbox to deactivate this area. Repeat with the formula @enddate. Then select OK to close the window. Now edit the sub report. The above procedure will have created two parameter fields in the sub report, {?Pm-@startdate} and {?Pm-@enddate}. These parameters represent the range value passed from the main report. You will now have to edit the record selection formula for the sub report. Select Report|Edit Selection Formula|Record from the menu and change the record selection to read: {table.datefield} >= {?Pm-@startdate} and {table.datefield} <= {?Pm@enddate}. Now, when the report is refreshed, the user will enter in the start and end dates in the main report and these values will populate the record selection formula in the subreport.

## How do I get page headers in a subreport?

There is no page header section available in a subreport. However, you can fake a page header if you've got at least one group in the subreport. Create your "page" header in the first header for the first group. In the suppression formula for that section, put the following: (pagenumber = 1 and not previousisnull({some key field})) or (pagenumber > 1 and not InRepeatedGroupHeader)

You need to be sure that "keep together" is turned on for the group. If it's not, you will not get the "page" header when a group breaks at the end of a page.