Lasso Soft Inc. > Home

  • Articles

How to have a good time with a Date

Avoiding mistakes and getting the output you need.

Session held at LDC Toronto May 2012.
Presented by Jolle Carlestam, Amtac Professional Services Pty Ltd

What about dates?

When working with web application development we soon run into a need to handle dates. We might want to:

  • log when something happened
  • give the user a chance to select a date or enter it
  • present a date on the page
  • do date calculations or manipulations

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!

Creating a date

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!

Getting output to look good

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.

Locales

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

Grabbing dates from database calls

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'

Hidden parts of a Lasso 9 date

A Lasso 9 date object holds more than meets the eye. Besides the date value it also always hold values for

  • Time
  • Milliseconds
  • Time zone
  • Daylight savings offset
  • Locale

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'

Validating dates

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.

Time zones

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)

?>

Grabbing users date using javascript

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);
	});

Excel magic

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

More info

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

Comments

No comments found
You must be logged in to comment.

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. > Home

 

 

©LassoSoft Inc 2015 | Web Development by Treefrog Inc | PrivacyLegal terms and Shipping | Contact LassoSoft