Archive for the 'Formulas' Category



Calculating the begin and end of Daylight Saving Time

Friday 16 August 2019 @ 11:32 pm

I am not a big fan of Daylight Saving Time (DST). I even hear some states and countries are talking about dropping it (yeah!). But in the meantime there are plenty of reports that need to adjust the time twice a year. This usually happens when the datetime values are stored in Greenwich Mean Time (GMT) and have to be converted to a local time. Then you need to know when DST begins and ends.

The first two formulas below calculate the beginning and ending dates of DST, based on the year of your transaction date. The third formula uses the first two formulas to make the one-hour adjustment. Substitute your GMT date fields into the first and third formula.

//DST Start
DateVar Start:= Date (Year ({@YourDateTimeGMT}) , 3, 15);
DateVar BOM:= Start - Day(Start)+7;
DateVar BOW:= BOM - DayOfWeek(BOM) + 8;
DateAdd('h', 2, BOW);

//DST End
{@DST Start} + 238 ;

//Adjusted DateTime
if {@YourDateTimeGMT} in {@DST Start} to {@DST End}
then DateAdd('h', 5, {@YourDateTimeGMT})
else DateAdd('h', 4, {@YourDateTimeGMT})




When that zero isn’t really a zero

Thursday 8 August 2019 @ 10:24 pm

Last week a customer was really befuddled. He had a formula that said:

if {@field} = 0 then ...

He could see lots of zero values but the formula didn’t work as expected.  He couldn’t figure it out so he sent it to me. The first thing I did was add a few more decimals to see if it was a rounding issue. That didn’t show anything, but the formula still insisted that the value was somehow NOT equal to zero.

So I went into the formula and multiplied the current value by one trillion. Then instead of zeros I started to see some small numbers.  I am not an expert on floating point values or database precision but I have seen this before in reports.  The solution is to round the value in the formula before comparing it to zero. In this case we rounded the value to two decimals like this:

if Round ({field}, 2) = 0 then ....

That made the formula behave as expected.  The odd part is that I have seen the same problem with two other customers in the past week. It could be just a coincidence, but I figured I would mention this and see if this is happening to more people.




Preventing the “division by zero” error

Friday 26 July 2019 @ 9:52 am

Crystal formulas can use 3 different divide operators:

  • Regular divide [ / ]
  • Integer divide [ \ ]
  • Percentage [ % ]

But all of these will fail if you follow the operators with a zero. The report will stop and Crystal will throw the error message “Division by Zero”.  The standard solution is to check and make sure the number you are dividing by is not zero before you do the calculation, something like this:

if {fieldA} = 0
then 0
else {fieldB} / {fieldA}

This way, whenever the bottom of the fraction (denominator) is a zero, the formula will print a zero and NOT try to do the calculation.
But even when customers use this formula pattern I still see the divide by zero error. Some users mistakenly check the top of the fraction (numerator) instead of the bottom (denominator).  Some do it correctly at first, but then change the denominator and forget to change the first line to match.  So I have developed the habit of using Local variables to make things easier. My normal pattern now looks like this:

Local Numbervar n:= {FieldA};//numerator
Local Numbervar d:= {FieldB}; //denominator
if d = 0 then 0 else n/d

This ensures that the value being checked is always the value on the bottom. Some other advantages of this method are:
1) If d is a subtotal or a long expression you only have to enter it in one place.
2) If you have to create a series of similar expressions, like for 12 different months, you can duplicate the first example and you only have to change the values at the top of the formula.




How to make sure you are on the last day of the month

Friday 19 July 2019 @ 9:18 am

Lots of reports require that you compare two different date periods and I often calculate prior date ranges based on the current range. But you have to be careful when the end of your prior period falls in a month with more days the the current period month?

Here is an example. Lets say your current period is in June and your prior period ends 6 months earlier in December. Calculating the Start Date is simple. If your Current Start is 6/1/2019 you can use a simple DateAdd like this:

DateAdd ('m', -6, {@CurrentStartDate})

but if you try a similar formula for the End Date you won’t get the right date. The following formula will return 12/30/2018:

DateAdd ('m', -6, {@CurrentEndDate})

The same issue occurs when your prior period is a year before and your current period ends in February. If he prior year is a Leap Year your prior period end date will be off by one day, ending on the 28th rather than the 29th.

My solution involves adding one day before you do the DateAdd and then subtracting that same day back out again, like this:

DateAdd ('m', -6, {@CurrentEndDate} +1) -1

This works because adding the one day puts on you on the first of the month. This is always a clean calculation when moving forward or backward by months or years. Then once you get to the first of the month in the prior period you subtract one day which always puts you on the last day of the month prior.




Finding groups where the last record meets a criteria

Monday 15 July 2019 @ 9:52 pm

One of my students presented me with the following challenge. Their address records were stored in a table that keeps an address history. That means that new addresses don’t replace the old addresses. Each new address is a new record with a time stamp. The current address is the record for that customer that has the latest timestamp.

To display the current address for each customer is fairly easy and can be done in one of two ways:

1) Group by Customer and sort by time stamp. Hide the details and place the address fields in the Group Footer. This would display the last address for each Customer.

2) Group by Customer and put in a group selection formula that says:

{Address.TimeStamp} = maximum ( {Address.TimeStamp} , {Address.CustomerID} )

Either of these will work to show the current address for each customer. But if you want to select only current addresses in a particular state, like NY, you have to be careful. If you put the State criteria in the record select expert or record selection formula the criteria will be applied before the grouping happens. Crystal will start by selecting only New York records regardless of how old the timestamp is. Then it will do the grouping and show the last NY record in each group. You would end up with the last New York address for each customer, rather than getting the accounts that have New York in their last record.  Anyone who had moved of NY to somewhere else would still show up.

My original solution involved a formula that combined the Date and the State into a single string field. Then I used a complicated group selection formula to find the right records. You can read about it here and it works fine.

But today I realized there is a simpler approach. The key is putting the State rule into the group selection formula, so it is applied after the grouping is done. So your Group Selection would look like this:

{Address.TimeStamp} = maximum ( {Address.TimeStamp} , {Address.CustomerID} )
and {Address.State} = "NY"

As long as the last line stays in the group selection formula this will return the desired records.




Using the ExtractString function

Friday 24 May 2019 @ 7:49 pm

I recently found a function in a Crystal Report that I hadn’t noticed before. Technically it is an additional function (UFL) but I am pretty sure it has been installed automatically with Crystal Reports for a long time. The file is dated 11/8/2000 so it might have been introduced with Crystal Reports V8.

The function is called ExtractString (). It is designed to locate two character strings within a longer string and return all the characters in between those two strings. I have done something similar using the InStr() function but it is much more complicated. A good example of a use for this is when you have XML tags in the middle of a long memo field and you want to extract the value between two specific tags. Say it looks something like this:

“blah blah blah <price>19.99</price> blah blah blah <price2>29.99</price2> blah blah blah “

To extract the value for price2 you would use the following formula:

ExtractString ( {table.xmlField}, '<price2>', '</price2>' )

You give the function three arguments:

  • The field you are searching
  • The string that marks the start
  • The string that marks the end

If it doesn’t find the start string it returns a blank (even if the end string is there).
If it finds the start string but no end string it returns everything after the start string.
if it finds both the start and the end it returns all the characters between them.
It skips over any end strings that occur before the start string.
If it finds multiple starts or ends it uses the first.

Next time I have to parse XML or do something similar I will use this function and save myself a few steps.




Using the function DrillDownGroupLevel

Tuesday 30 April 2019 @ 4:42 pm

Crystal has a function that calculates the drill-down level of the current window. This allows you to have objects and sections format differently at specific levels of drill-down. The function is called DrilllDownGroupLevel and it gives you a numeric value that tells you the current drill-down level for any preview tab.  The normal preview is level 0, the first drill-down is level 1, etc. So if you want a section with column headings to appear at the second level of drill-down and ONLY at the second level of drill-down you can use the following as a suppression formula for that section:

DrilllDownGroupLevel <> 2

That section will be suppressed in normal preview (drill-down level 0) and also at the first drill-down level, but will appear if you drill down again from level 1. If you are ever unsure which level of drill-down you are on you can write a formula that says simply:

DrillDownGroupLevel

Place this formula in the section in question. When you see that section the number shown will tell you the current drill-down level.

DrillDownGroupLevel also helps you solve one of the dilemmas of report design.  One of the lessons in my advanced class is how to have a report be either a detailed report or a summary report, based on a parameter prompt. You accomplish this with a parameter that has two choices (Summary/Detail) and you use that parameter in the detail section’s suppress formula. It has to be the suppress property because the “hide” property doesn’t have a condition formula button. And, because you are using the suppress property you can no longer drill-down from the summary version of the report to see the details for a group.

But by using the following as your suppress formula you can have the parameter to choose summary or detail, and still have drill-down available on the summary version:

{?Parameter} = "Summary" and
DrillDownGroupLevel <> 1

This will suppress the section when you choose “Summary”, but only as long as you are in the main preview tab.  As soon as you drill down the suppress condition will no longer be met and the details will be visible.




CurrentDate vs DataDate

Tuesday 23 April 2019 @ 11:20 pm

There are two ways to use today’s date in a Crystal formula. These are useful when you want the report to automatically determine a date range, like the last three days. You can calculate the date for three days ago by using one of the following calculations:

CurrentDate - 3

or

DataDate - 3

These expressions will generate the same value at the time the report is refreshed. But if you interact with reports after refreshing them, or if you open reports with saved data, it is important to know how these functions differ.

CurrentDate is identical to the functions PrintDate and Today. PrintDate is also a special field. These date function will all update whenever one of three things happens. When the report is:

1) opened
2) previewed after a modification.
3) printed

Interestingly, exporting a report to a PDF does NOT update these dates.

DataDate is a function and is also a special field. These are updated when the the report is refreshed.

So lets say I ran a report yesterday, saved it with data yesterday and then reopened it today without refreshing it. The CurrentDate function will show today’s date while the DataDate function will show Yesterday’s date.

You can see how these functions could affect record selection. If I calculate criteria to include the last 3 days using the CurrentDate function, and then reopen the report later with saved data, the CurrentDate will change and so will the criteria. The saved data will be reduced or eliminated. However, if I wrote the same criteria using DataDate there is no change because I have not refreshed the report.  So when deciding to use one of these functions you should think about how the formula should respond when you reopen the report with saved data. If it should use date when the report is opened then use CurrentDate.  If it should use the date when it was refreshed then use DataDate.

Also note that there are corresponding time functions (CurrentTime, PrintTime) and special fields that have the same names.   The time functions follow the same pattern as the date functions.




Another option for selecting ALL in a parameter

Saturday 23 March 2019 @ 11:04 am

I wrote last fall about selecting “ALL” with a string parameter field. My comments at the end list options for making the technique work for numbers and dates. Recently I received a note from one of my colleagues on a better approach, using optional parameters.

Starting with CR 2008 (v14) Crystal has allowed us to define a parameter as “optional”. This allows the user to not enter a value at all in a parameter. However, when you use an optional parameter in a formula you always have to test for the existence of a value in the parameter field before you use. The test often looks like this:

if HasValue ( {?Tax Rate} )
then {Orders.Order Amount}*{?Tax Rate}
else 0

This way the report knows what to do when the parameter is skipped.

Another way of creating an ALL option is to tell the report that any time there is no value entered in the parameter, the user wants ALL values. The formula would look something like this:
(not HasValue ( {?State} ) or {Customer.State} = {?State})

Note that the HasValue() test has to come before any other test that uses that parameter. If you reverse the two tests in this formula it will generate an error whenever you don’t fill in the parameter.

And thanks to Luc Rascar, a Crystal Reports/Business Objects consultant in France, for pointing this out.




Carriage returns in a formula that will survive a text export.

Friday 8 March 2019 @ 12:40 am

There are several common uses for exporting to text format.

I use text format whenever I need a Fixed Length export file. These are files where the exported record doesn’t have a comma or pipe delimiter. Each field in the string is identified by its character position in the string, since each field is a specific number of characters. This means that all records end at the same position, regardless of how long individual field values are.

I also use text export format for some CSV exports, because there is more flexibility.  For instance, when I need to generate two CSV rows from the same detail row in the data I find it easier to structure the CSV rows in a formula  and export as text.

And that brings me to what I learned last week. If you are exporting to “text” format and the formula you are exporting has carriage returns in it, you might find that they don’t work after the export. For instance the formula below would show 3 rows in the preview of Crystal Reports:

{@String1} & CHR(13) &
{@String2} & CHR(13) &
{@String3}

The function element CHR(13) creates a carriage return between the different elements of the string. But if you export this formula using “text” format you will find that the carriage returns don’t survive the export. The text file would not have the three rows that you see in preview. But with a little experimenting I found that adding a second related function in the formula works better:

{@String1} & CHR(13) & CHR(10) &
{@String2} & CHR(13) & CHR(10) &
{@String3}

In CR preview both formulas will appear the same. However, the second formula will provide carriage return that survives into the text export, while the first one will not.

Within a week of making this discovery for one customer, I found I needed the same thing for a second customer. I probably should have figured this out even sooner. When working text files and hidden codes I have seen that you usually need both a carriage return, Chr(13), and a line feed , Chr(10) to start a new line. But since it only takes one of these in Crystal preview, it is easy to forget that they work together.




«« Previous Posts
Jeff-Net

Recrystallize Pro

Crystal Reports Server