Avoiding mistakes and getting the output you need.
Session held at LDC Toronto May 2012.
Presented by Jolle Carlestam, Amtac Professional Services Pty Ltd
When working with web application development we soon run into a need to handle dates. We might want to:
Initially date handling can look deceivingly simple. Just create the date object using Lassos date type
var(mydate = date)
or create it based on the user input
var(hisdate = action_param('thedate'))
But once we start digging into date handling we discover that it's often more complex than that.
Handling user input can be a challenge given the vast variety of ways that users would want to write dates. Same goes with presenting dates. Are your web site visitors from US, Canada, Germany, Egypt or China? They have different demands and expectations on how a date looks.
Same goes with performing date calculations or manipulations. A Lasso 9 date object is more that just a date. It automatically also include a time value, milliseconds, time zone, daylight saving offset and locale as well. Knowing about the complexity and the powers of the date type and object soon becomes a necessity.
This paper is a companion to the seminar presentation held at LDC in Toronto May 2012. It is not a stand alone work. The focus is mainly to provide code examples related to what was presented in the seminar. It is probably of some help to also have access to the slides that were used in the presentation. And of course, the best thing would be if you'd attended the seminar yourself...
All the code examples have been tested and should work in any version of Lasso 9.1.X. Feel free to copy them and test them in your own environment. Or use them in your projects. To preserve space most occurrences of delimiters like < br > and < hr > have been removed. You might want to reinsert them here and there.
On we go!
Lasso 9 provide a date type called date
If called without params it will create a date object representing the present date and time for the server it is called on.
var(mydate = date)
$mydate
2012-05-25 12:10:53
You can create the date object using input params
var(mydate = date('2012-05-25'))
var(mydate = date(action_param('date')))
Note that even though the input lacked a time section the created date object will still have a time value
$mydate
2012-05-25
$mydate -> format('%q')
20120525120000
What the time value will be in the created date object, if not set explicitly, is something the server makes up and can't be predicted.
It's also possible to create a date object by specifying its different parts
var(mydate = date(-year = 2012, -month = 05, -day = 25))
2012-05-25 12:33:50
Any value that's not set when using the above technique will be populated with the same value it would have had the date object been called without any params at all
var(mydate = date(-year = 2011, -day = 25, -hour = 20))
2011-05-25 20:12:49
Dates are stored by Lasso 9 as an integer representing seconds. Starting from 1/1 1970. That makes it possible to create a date object with an integer as input param.
var(mydate = date(1337941089))
2012-05-25 12:18:09
It is actually also an option to provide a decimal as input. The decimal fraction represents milliseconds
var(mydate =date(1337941089.576))
$mydate -> millisecond
576
Accommodating different input formats
Given the multitude of ways a date can be presented we need to deal with users wanting to supply dates in different formats.
Lasso 9 knows how to interpret input as a date provided it's in either ISO or the US date format.
An ISO date looks like this
2012-05-25
Starting with the year using four digits followed by the month and ending in the day.
If time is part of the object it will look like this
2012-05-25 13:10:55
The hour value is always displayed using a 24 hour clock. There is no AM/PM in an ISO datetime.
To make sure Lasso understands what to do with the provided date param it's usually a good idea to instruct it what format the input is in.
It's done using the -format param
var(mydate = date('25/05/2012', -format = 'dd/MM/Y'))
The -format param is a string with instructions for what to expect where. There are numerous choices available for the -format param. It is possible to use the Lasso 8.X type of format. They mostly start with a percentage sign
var(mydate = date('25/05/2012', -format = '%d/%m/%Y'))
Lasso 9 also supports the full set of ICU date/time formatting and parsing flags
var(mydate = date('25/05/2012', -format = 'dd/MM/yyyy'))
var(mydate = date('25 may 2012', -format = 'dd MMMM yyyy'))
You can read more about ICU formatting here http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax
You can mix literal characters with formatting flags. Like the slashes and spaces in the examples above. If a character can have dual meanings the literal character needs to be escaped.
If a date object is created using a -format param it will keep that format and use it when output. This is important to remember since some cases require the provided date to be of a specific format. Like when sending the date to a Mysql record. Mysql requires dates to be formatted as ISO. All of the above examples will fail if sent as is to a Mysql query. For the database to accept them you need to format them first. Any of these work for Mysql
$mydate -> format('%Q')
$mydate -> format('%q')
$mydate -> format('Y-MM-dd')
2012-05-25
20120525121809
2012-05-25
Oh, and be careful when assuming that input is using text representation of the month. Lasso will look for text values based on the default locale setting it currently run under. This code will fail if locale_default is sv_SE (Swedish)
var(mydate = date('25 may 2012', -format = 'dd MMMM yyyy'))
$mydate
25 januari 2012 // wrong!
If in doubt set the default _locale first
locale_setDefault(locale('en','CA'))
var(mydate = date('25 may 2012', -format = 'dd MMMM yyyy'))
$mydate
25 May 2012 // right!
More about locales later!
You can format the way Lasso will output a date in several ways. Either by telling the date object what format to use with the method or by applying a format param when outputting it. All formatting options that were available when formatting input are of course available for output.
To set a format on the date object itself
var(mydate = date('2012-05-25'))
$mydate -> setformat('dd MMMM yyyy')
$mydate
25 May 2012
or set the format when outputting the date value
$mydate -> format('MMMM dd, yyyy')
May 25, 2012
Remember that output formatted with -> format is a string. Look at these code examples for the difference
var(mydate = date('2012-05-25', -format = 'Y-MM-dd'))
$mydate
$mydate -> type
var(mydate = date('2012-05-25') -> format('Y-MM-dd'))
$mydate
$mydate -> type
2012-05-25
date
2012-05-25
string
The output looks the same, but the first var is a date object, the other a string.
Lasso format options allow you to mix the output with static text. It's perfectly ok to format the output to "year 2012". But, since most characters are reserved as flags for the formatting, you need to escape them. In the above example y = year, e = local day of week, a is the AM/PM marker. The only character that's not reserved is r, but since Lasso don't know what to do with it is skipped altogether. Here's the undesired outcome
$mydate -> format("year %y")
20126PM
You need to somehow tell Lasso what's to be treated as literal characters and what's not. This is done by escaping the characters that's to be treated as literals. In Lasso usually by backslashing. However, Lassos ICU formatting is not happy with that. It frowns upon backslashed characters. Instead it expects the literal characters to be within single quotes. Under some circumstances this can confuse Lasso as to what you want exactly since Lasso has it's own perception of what quotes are for.
Enter the backticks! Lasso 9 offers the option to use backticks as enclosing quote signs. All of these are allowed Lasso 9 syntax
var(myvar = 'My value')
var(myvar = "My value")
var(myvar = `My value`)
The special thing with backticks is that they treat everything inside them as it is written. Very handy for writing regular expressions but it's also handy when you need to write complex formatting instructions for a date object. Here's a real life example. We needed a specific format to send to a remote server that had strict rules for date input
$mydate -> format(`yMMdd'T'HHmmss'Z'`)
20120525T170000Z
Without the backticks the above output would have been much harder to accomplice. Please try if you don't believe it. But believe me, we've tried.
When you display dates in the browser it can be cumbersome to localize the output. Say you have this excellent web application that was developed for the US market. You want dates to be presented for the visitors in a format they recognize. In the US they don't understand ISO formatted dates thus you have your code littered with
$mydate -> format('MMMM dd, Y')
May 25, 2012
But, enter the need to expand your service to the Canadian market. Canadians don't write dates like that. They probably understand it but it irritates them that this know-nothing-american can't get it right. So, what's to do? A global search and replace for all format strings replacing them with
$mydate -> format('dd MMMM, Y')
25 May, 2012
But that's not a good long term option. Are you supposed to keep two code bases just to get dates presented in a proper way? And what if you expand to additional markets? Latin America, Germany, China?
Enter locales. A way to let Lasso do the formatting for you, based on the locale setting in the server. Or, with a few lines of code, based on client and/or user preference.
To format a date based on the locale setting
locale_default->format($mydate, 1)
or
locale_default->format($mydate)
If the locale_default is set to en_US the above would render
May 25, 2012
May 25, 2012
If set to en_CA the same code will output
25 May, 2012
2012-05-25
To set the default locale
locale_setDefault(locale('en','US'))
locale_setDefault(locale('fr','CA'))
locale_setDefault(locale('sv','SE'))
This can be done once, for example when you initiate all site variables and the setting will then stick until Lasso's done processing the request.
Locales are not only useful for date formatting. It deals equally well with integer and decimal formatting as it does with time and currency. And more. Read up on it on Lasso reference. And ask Kyle/Jono to complete the reference with more examples, like how to use the powerful -ordinal flag...
If you want to find out what locales Lasso 9 knows about here's a handy code snippet
iterate(locale_availablelocales) loop_value '<br>' loop_value->format($mydate) '<br>' loop_value->format($mydate, 1, 1) '<hr>' /iterate
A Mysql field, despite that it's set to date or datetime, will return a value of type string. Provided that it has content. If not it will return NULL.
This affects how you turn the value into a usable and correct date object. If the field has content then there's no worries. Just set the field content to a date variable
field('f_date')
date(field('f_date'))
date(string(field('f_date')))
2012-05-25
2012-05-25
2012-05-25
But if the field is NULL the result is dramatically different
// returns NULL
01/01/0001 00:00:00
2012-05-25 12:23:13
None of the date values are the desirable result. Pending on your needs you need to trap for this
var(mydate = (string(field('f_date')) -> size > 0 ? date(field('f_date')) | NULL))
Be aware that with the above trap the var mydate can be NULL. So don't perform date specific methods on it without checking that it's a date first. Calling for example NULL -> format('%Q') will result in an error.
$mydate -> isa(::date) ? $mydate -> format('%Q') | 'No date presented'
A Lasso 9 date object holds more than meets the eye. Besides the date value it also always hold values for
The hidden parts of a date makes the date object a really powerful thing. Being able to grab or redefine a dates time zone is useful. To be able to tell with a simple call if a specific date is using daylight savings or not is cool.
But the hidden parts can also act as a nuisance. If you forget about the milliseconds comparing two datetime values can show false when you'd expect it to show true etc. To prevent that, make sure you create the date object with milliseconds set to 0
date(date -> format('%q'))
Or, if you only need to compare 2 dates and don't care about the time, here's a tip from Jono
date(field('mydate')) -> julianDay == date -> julianDay
// generates a boolean true or false
To display the different parts of a date object is easy, there is usually a method you can call to grab the value.
$mydate -> time
$mydate -> millisecond
$mydate -> timezone
$mydate -> zoneOffset
$mydate -> dst
$mydate -> dstOffset
12:21:27
787
America/Toronto
-18000000
true
3600000
As you can see the offset methods returns milliseconds.
To change a value can be equally simple. Most methods that get a value can also be used to set it. For numeric type of values, setting the new value can be done using += or -= etc.
$mydate -> hour = 12
$mydate -> millisecond = 0
$mydate -> month += 10
$mydate -> timezone = 'GMT'
There is an existing method to ensure that a string is a valid date, valid_date() but it comes with a flaw. It will accept anything that can be interpreted as a date and return true if that's the case. Including impossible dates.
valid_date('2011-02-29')
true
The above date doesn't exist. But Lassos date type comes with a feature that will take impossible date values and convert them into something that's correct. Thus converting 2011-02-29 to 2011-03-01, and since that date does indeed exist valid_date will return true.
If you're not happy with that, here's a replacement for the built in valid_date method that adds a param -strict. It will return false if the date is not a real date.
valid_date('2011-02-29', -strict)
false
Put the following code in your LassoStartup directory and restart your instance
<?LassoScript /**! define valid_date Replacement of valid_date that adds an optional param -strict. When set to true it will fail on dates that doesn't exist in the real world. Like 2010-02-31. When used with strict assumes that date inputs are either as ISO or US date format. Note that it's mute to test a created date object using strict since making it a date object will change the input to a valid date before valid_date get a chance to test it. Ie this is pointless: valid_date(date('2011-02-29'), -strict) since Lasso will transform it to 2011-03-01 before valid_date tests it. This however will return the correct false: valid_date('2011-02-29', -strict) Written by Jolle Carlestam 2012-05-20 JC Changes in signature 2012-05-08 JC Slight modification of syntax including aligning format section with the latest from Lassosoft repo 2010-11-02 JC First version **/ define valid_date(indate::any, format::string = '', strict::boolean = false) => { log_critical('calling modified valid_date') // Empty input #indate == null ? return(false) string(#indate)->trim& == '' ? return(false) local(_format = #format -> ascopy) // Parse date local(parse) = (#format != '' ? date(#indate, -format=#format) | date(#indate)) // Invalid dates (#parse->type == 'null') ? return(false) (string(#parse)->size == 0) ? return(false) if(#strict) => { // strict will check that a date actually exist in the real world // find out input format, we can't convert input to a date type since that will change the input values if needed and we need to avoid that local(date_array = (string(#indate) -> split(' ')) -> first) // get rid of the time part match(true) => { case(#_format == '%D') // US date format #date_array = #date_array -> split('/') (#date_array -> size != 3 ? return false) local(day = #date_array -> second) local(month = #date_array -> first) local(year = #date_array -> last) case(#_format == '%Q' || #_format == '') // assume ISO format since that is standard in Lasso 9 #date_array = #date_array -> split('-') (#date_array -> size != 3 ? return false) local(day = #date_array -> last) local(month = #date_array -> second) local(year = #date_array -> first) case return false } (integer(#year) < date -> min(-year) || integer(#year) > date -> max(-year) ? return false) (integer(#month) < 1 || integer(#month) > 12 ? return false) (integer(#day) < 1 || date(#year + '-' + #month + '-01') -> month(-days) < integer(#day) ? return false) else(#format != '') // Strict check of format against original date (allowing for leading zeroes or spaces) string(#indate) == #parse->format(regexp(-find='%-?_?', -replace='%')->replaceall(-input=#format)) ? return(true) string(#indate) == #parse->format(regexp(-find='%-?_?', -replace='%_')->replaceall(-input=#format)) ? return(true) string(#indate) == #parse->format(regexp(-find='%-?_?', -replace='%-')->replaceall(-input=#format)) ? return(true) return(false) } return(true) } define valid_date(indate, -format::string = '', -strict::boolean = false) => valid_date2(#indate, #format, #strict) ?>
Now, in order for the adjusted valid_date method to work, you need to deal with a bug in Lassos date type. If left as is
date -> month(-days)
will always return 31 regardless of what month you're asking for. Pending that LassoSoft fixes that here's a replacement you can use. And have to use if you want to use the altered valid_date method.
Place the following code in your LassoStartup directory and restart your instance
<?LassoScript /**! date->month Bugfix that makes date -> month(-days) return a correct value. Without it Lasso 9 returns 31 regardless of what month date contains Written by Jolle Carlestam **/ define date->month( -long::boolean=false, -short::boolean=false, -days::boolean=false ) => { #long ? return .format( '%B' ) #short ? return .format( '%b' ) if(#days) local(tempdate = date(.format('%Q'))) #tempdate -> day = 1 #tempdate -> month += 1 #tempdate -> day -= 1 return #tempdate -> day /if return ..get(ucal_month)+1 } ?>
Note that since both of the above code snippets replaces Lassos built in methods you need to place the code in LassoStartup. Putting it in LassoLibraries won't do since they would never be called from there. Lasso 9 would have found the built in methods and be satisfied with that.
Sometimes you want to change a dates time zone value. That can be really easy
date -> timezone = 'America/Toronto'
But if you need to convert a date object from one time zone to another including actually changing the values it gets trickier. Here's a method you can call to help you with that
<?LassoScript /**! tzconvert Method to set datetime values to specific timezones. Can also convert datetime values from the input timezone to the output. Params are: date Input date, required -tz_in What timezone the input date is to be treated as. If not set will use the timezone from the input date -tz_out What timezone the output date should be set to. Defaults to GMT -format Format used for the output date. Defaults to %Q %T (2011-08-26 12:58:27) -convert If true will convert the input date to a value corresponding to the output timezone. Defaults to false NOTE Requires Lasso 9.1 or a late SVN patched version of Lasso 9.0 due to changes/fixes made to the date type Usage examples: var(mydate = date('2012-05-25 12:30')) tzconvert($mydate, -tz_out = 'Australia/Sydney') -> timezone -> Australia/Sydney tzconvert($mydate, -tz_in = 'GMT', -tz_out = 'Australia/Sydney', -convert) -> 2012-05-25 22:30:00 tzconvert($mydate, -tz_in = 'GMT', -tz_out = 'Australia/Sydney', -format = '%Q %T V', -convert) -> 2012-05-25 22:30:00 AEST tzconvert($mydate, -tz_out = 'America/Toronto') -> timezone -> America/Toronto tzconvert($mydate, -tz_out = 'America/Toronto', -format = 'V') -> GMT-05:00 Written by Jolle Carlestam 2011-08-31 JC First version **/ define tzconvert( date::date, input_timezone::string = #date -> timezone, output_timezone::string = 'GMT', format::string = '%Q %T', convert::boolean = false ) => { local(_date = date(#date -> format('%q'))) #_date -> timezone = #input_timezone local(zoneoffin = #_date -> ascopy -> zoneoffset) #zoneoffin += #_date -> ascopy -> dstoffset #_date -> timezone = #output_timezone local(zoneoffout = #_date -> ascopy -> zoneoffset) #zoneoffout += #_date -> ascopy -> dstoffset #convert ? #_date -> add(-millisecond = (#zoneoffout - #zoneoffin)) #_date -> setformat(#format) return #_date } define tzconvert( date::date, // input date -tz_in::string = #date -> timezone, -tz_out::string = 'GMT', -format::string = '%Q %T', -convert::boolean = false ) => tzconvert(#date, #tz_in, #tz_out, #format, #convert) ?>
On our login page we have some javascript that allows us to grab the browsers date, time and GMT offset. It is put into hidden fields of the login form and sent along with the login credentials. On the server it is then stored in the users session. The script needs Jquery to populate the form fields
var curDate = new Date() var offsetGMT = (-(curDate.getTimezoneOffset()/60)) var curHour = curDate.getHours() var curMin = curDate.getMinutes() var curSec = curDate.getSeconds() var curTime = ((curHour < 10) ? "0" : "") + curHour + ":" + ((curMin < 10) ? "0" : "") + curMin + ":" + ((curSec < 10) ? "0" : "") + curSec var curYear = curDate.getYear() if(curYear<1000) curYear+=1900 var curMonth = (curDate.getMonth()+1) var curDay = curDate.getDate() var curDateTime = curMonth+"/" + curDay+ "/" + curYear+ " "+ curTime $(function(){ $("#GMT_Offset").val(offsetGMT); $("#LDT").val(curDateTime); });
Finally a tip useful when there's a need to convert an Excel date value to Mysql or Lasso. Excel stores datetime as an integer expressing the number of days since Jan 1 1900. Decimals express time of day.
Lasso (and Mysql?) stores datetime as an integer expressing the number of seconds since Jan 1 1970. Decimals express milliseconds.
If you're in a situation where you need to import raw Excel data into Mysql, or for processing by Lasso, you need to convert the Excel datetime integer to a relevant Mysql/Lasso equivalent.
This sql query will help you with the Mysql handling
SELECT FROM_UNIXTIME(([Excel_integer] - 25569) * 86400) AS newdate
Example
SELECT FROM_UNIXTIME((41054 - 25569) * 86400) AS newdate
2012-05-25 02:00:00
To perform the same magic using Lasso
var(timestamp = (41054 - 25569) * 86400)
date($timestamp)
2012-05-25 02:00:00
You can read more about dates and date handling here
That's it
Enjoy!
Written in Lund May 2012
Jolle Carlestam
Author: Jolle Carlestam
Created: 25 May 2012
Last Modified: 26 May 2012
Please note that periodically LassoSoft will go through the notes and may incorporate information from them into the documentation. Any submission here gives LassoSoft a non-exclusive license and will be made available in various formats to the Lasso community.
©LassoSoft Inc 2015 | Web Development by Treefrog Inc | Privacy | Legal terms and Shipping | Contact LassoSoft