Thursday, July 31, 2008

Sorting a numbered list

One task that's quite common is the sorting of data in an array. But there are some pitfalls to bear in mind, particularly when it comes to sorting numbers.

For example, take the following simple list and apply a sort:


var list:Array = [5, 10, 2, 21, 1, 15];
list.sort();
trace(list);
// outputs [1, 10, 15, 2, 21, 5]

Yikes...that didn't go exactly according to plan! So what happened? By default, the Array.sort() method converts numbers to strings before they're sorted, and then it sorts them according to their Unicode value.

Fortunately, Flash provides an option to perform a numeric sort on an array containing numbers:


var list:Array = [5, 10, 2, 21, 1, 15];
list.sort(Array.NUMERIC);
trace(list);
// outputs [1, 2, 5, 10, 15, 21]

Flash treats the contents of the array as numeric values and performs a correct sort. Which is fine, but what if the contents of the array are already stored as strings?


var list:Array = ["5", "10", "2", "21", "1", "15"];
list.sort(Array.NUMERIC);
trace(list);
// // outputs [1, 10, 15, 2, 21, 5]

Uh-oh, it looks like we're back to square one again!

Fortunately, it's easy to create a function that can correct this default behaviour by comparing each individual element in the array:


function compare(a, b) {
return a - b;
}

var list:Array = ["5", "10", "2", "21", "1", "15"];
list.sort(compare);
trace (list);
// outputs [1, 2, 5, 10, 15, 21]

The same function works with numbers too:


var list:Array = [5, 10, 2, 21, 1, 15];
list.sort(compare);
trace (list);
// outputs [1, 2, 5, 10, 15, 21]

And it will even perform a correct numerical sort with a mix of numbers and strings:


var list:Array = [5, "10", 2, "21", "1", 15];
list.sort(compare);
trace (list);
// outputs [1, 2, 5, 10, 15, 21]

If you want to reverse the order of your sort, the function handles that too; just swap the arguments around before they're returned:


function reverse(a, b) {
return b - a;
}

Wednesday, July 30, 2008

When the heck is Easter?

Thanks to some arcane historical, and religious reasons, Easter has a tendency to slide around the calendar like a drunk man on an ice-rink. Fortunately it is still possible to calculate when it occurs in Flash with the following function:

function calcEaster(year) {
Months = new Array("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December");
month = Math.floor((((19 * (year % 19) + Math.floor(year / 100) - Math.floor(Math.floor(year / 100) / 4) - Math.floor((Math.floor(year / 100) - Math.floor((Math.floor(year / 100) + 8) / 25) + 1) / 3) + 15) % 30) + (32 + 2 * (Math.floor(year / 100) % 4) + 2 * Math.floor((year % 100) / 4) - ((19 * (year % 19) + Math.floor(year / 100) - Math.floor(Math.floor(year / 100) / 4) - Math.floor((Math.floor(year / 100) - Math.floor((Math.floor(year / 100) + 8) / 25) + 1) / 3) + 15) % 30) - (year % 100) % 4) % 7 - 7 * Math.floor(((year % 19) + 11 * ((19 * (year % 19) + Math.floor(year / 100) - Math.floor(Math.floor(year / 100) / 4) - Math.floor((Math.floor(year / 100) - Math.floor((Math.floor(year / 100) + 8) / 25) + 1) / 3) + 15) % 30) + 22 * (32 + 2 * (Math.floor(year / 100) % 4) + 2 * Math.floor((year % 100) / 4) - ((19 * (year % 19) + Math.floor(year / 100) - Math.floor(Math.floor(year / 100) / 4) - Math.floor((Math.floor(year / 100) - Math.floor((Math.floor(year / 100) + 8) / 25) + 1) / 3) + 15) % 30) - (year % 100) % 4) % 7) / 451) + 114) / 31) - 1;
month = Months[month];
day = ((((19 * (year % 19) + Math.floor(year / 100) - Math.floor(Math.floor(year / 100) / 4) - Math.floor((Math.floor(year / 100) - Math.floor((Math.floor(year / 100) + 8) / 25) + 1) / 3) + 15) % 30) + (32 + 2 * (Math.floor(year / 100) % 4) + 2 * Math.floor((year % 100) / 4) - ((19 * (year %
19) + Math.floor(year / 100) - Math.floor(Math.floor(year / 100) / 4) - Math.floor((Math.floor(year / 100) - Math.floor((Math.floor(year / 100) + 8) / 25) + 1) / 3) + 15) % 30) - (year % 100) % 4) % 7 - 7 * Math.floor(((year % 19) + 11 * ((19 * (year % 19) + Math.floor(year / 100) - Math.floor(Math.floor(year / 100) / 4) - Math.floor((Math.floor(year / 100) - Math.floor((Math.floor(year / 100) + 8) / 25) + 1) / 3) + 15) % 30) + 22 * (32 + 2 * (Math.floor(year / 100) % 4) + 2 * Math.floor((year % 100) / 4) - ((19 * (year % 19) + Math.floor(year / 100) - Math.floor(Math.floor(year / 100) / 4) - Math.floor((Math.floor(year / 100) - Math.floor((Math.floor(year / 100) + 8) / 25) + 1) / 3) + 15) % 30) - (year % 100) % 4) % 7) / 451) + 114) % 31) + 1;
}


The following code will calculate the date of Easter between the years 1950 and 2150. It uses the getOrdinalNumber function discussed elsewhere on this blog:

for (i = 1950; i <= 2150; i++) {
calcEaster(i);
trace("Easter falls on Sunday " + getOrdinalNumber(day) + " of " + month + ", " + i);
}

Ordinal Numbers in Flash

We use ordinal numbers all the time, even though we may not always appreciate what they are, or why we're using them.

An ordinal number reflects the rank of that number in a particular order, or its position. So we might use expressions like "he came first in the race", or "that's the third bus to come along in the past hour", or "today is the thirtieth of July". When we write down ordinal numbers we use shorthand, so first becomes 1st, third becomes 3rd, and thirtieth becomes 30th.

Here's a (relatively) simple function that can calculate the ordinal for any given number in Flash:

function getOrdinalNumber(num) { 
return num==0 ? num : num + ['th', 'st', 'nd', 'rd'][!(num % 10 > 3 || Math.floor(num % 100 / 10) == 1) * num % 10];
}


Using the function is straightforward:

trace(getOrdinalNumber(11));
// returns 11th
trace("Today is Thursday " + getOrdinalNumber(7) + " February");
// returns Today is Thursday 7th February
for(i=0; i<=120; i++){
trace(getOrdinalNumber(i));
}
// returns all of the ordinal numbers from 1 to 120

Calculating the nth Date in Flash

One thing that's not easy to calculate in Flash is the occurrence of floating dates. For example, in the US, Thanksgiving Day occurs on the fourth Thursday of November whereas, in Canada, it occurs on the second Monday of October. But how do you calculate which is the fourth Thursday or the second Monday in any given month?

The answer is this function:

function nthDay(nth, weekday, month, year) {
var lookup:Object = {first:1, second:2, third:3, fourth:4, fifth:5, last:0, firstlast:-1, secondlast:-2, thirdlast:-3, fourthlast:-4, fifthlast:-5, penultimate:-1, antepenultimate:-2, preantepenultimate:-3, sun:0, mon:1, tue:2, wed:3, thu:4, fri:5, sat:6, sunday:0, monday:1, tuesday:2, wednesday:3, thursday:4, friday:5, saturday:6, jan:0, feb:1, mar:2, apr:3, may:4, jun:5, jul:6, aug:7, sep:8, oct:9, nov:10, dec:11, january:0, february:1, march:2, april:3, may:4, june:5, july:6, august:7, september:8, october:9, november:10, december:11};
for (i in lookup) {
if (nth.toLowerCase() == i) {
nth = lookup[i];
}
if (weekday.toLowerCase() == i) {
weekday = lookup[i];
}
if (month.toLowerCase() == i) {
month = lookup[i];
}
}
var nthDate:Date = new Date(year, month + ((nth <= 0) ? 1 : 0), 1);
var dayofweek:Number = nthDate.getDay();
var offset:Number = weekday - dayofweek;
nthDate = new Date(year, month + ((nth <= 0) ? 1 : 0), nthDate.getDate() + (offset + (nth - (offset >= 0 ? 1 : 0)) * 7));
if (nthDate.getMonth() <> month) {
return "**ERROR ** nthDay (" + nth + ") Out of range";
} else {
return nthDate;
}
}

The function accepts four arguments:
  • nth - an integer that represents which nth day occurrence to search for, e.g. 1, 2, 3, 4, 5
    n.b. A zero value will return the date of the last weekday.
    n.b. A negative value will return the nth day counting back from the end of the month.
  • weekday - day of the week to search for (Sunday = 0, Monday = 1, etc.)
  • month - month to search in (January = 0, February = 1, etc.)
  • year - year to search in (1995, 2008, etc.)
The function returns a new Date Object (nthDate) containing the nth occurrence unless there's an error, in which case it will return an error message.

So, to calculate Thanksgiving Day, you would use:

trace("US Thanksgiving Day = " + nthDay(4, 4, 10, 2008));
// outputs Thu Nov 27 00:00:00 GMT+0000 2008
trace("Canada Thanksgiving Day = " + nthDay(2, 1, 9, 2008));
// outputs Mon Oct 13 00:00:00 GMT+0000 2008


The function can be used to calculate any possible occurrence. For example, you can use it to work out the last Tuesday in a month with nthDay(0, 2, 10, 2008).
You can use it to count backwards from the end of the month so, to find the last but one Tuesday in a month, you would use nthDay(-1, 2, 10, 2008);

You can also combine the function with existing date objects:

today = new Date();
nthDay(0, 6, today.getMonth(), today.getFullYear()));
// will trace the last Saturday of the current month
trace("= " + nthDay(2, today.getDay(), today.getMonth() + 2, today.getFullYear()));
// will trace the second occurrence of this weekday in two months time


Last, but by no means least, the function lets you replace all those ugly numbers with user-friendly string references such as "first", "second", "last", "nov", "tues", "penultimate", etc.

Instead of writing nthDay(-2, 2, 10, 2008) you can use:
nthDay("secondlast", "tue", "oct", 2008) or
nThDay("antepenultimate", "tuesday", "October", 2008)

Thanksgiving Day in the US would then be written thus:
nthDay("fourth", "thursday", "november", 2008)
while the equivalent holiday in Canada would be:
nthDay("second", "mon", "oct", 2008)

Validating Dates in Flash

Having seen a plethora of functions that attempt to validate dates in Flash, almost all of them involving various methods of splicing strings and a whole heap of conditional if statements to check for leap years, month lengths, etc., I thought it would probably be a lot simpler to let the Date Object itself do the checking:

function isValidDate(day, month, year):Boolean {
var d:Date = new Date(year, --month, day);
return d.getDate() == day && d.getMonth() == month && d.getFullYear() == year;
}

As you can see, the function accepts three arguments; the day, month, and year; and it returns a boolean true or false depending on whether the date is valid or not.

Using the function is simplicity itself:

trace("Checking 31/3/2008: " + isValidDate(31, 3, 2008)); // returns true
trace("Checking 31/4/2008: " + isValidDate(31, 4, 2008)); // returns false

To validate dates that are entered in a different format, e.g. for US-style dates (mm/dd/yyyy) just swap the order of the function arguments:

function isValidDate(month, day, year) {
}

If you still want to validate the correct use of date delimiters, you can do so quite easily by first splitting the date into an array and then checking against each index. The function will continue to return true or false, as this example demonstrates:

var date_str:String = "28/2/2008";
var date_arr:Array = date_str.split("/");
trace(isValidDate(date_arr[0],date_arr[1],date_arr[2]));
// returns true
var date_str:String = "28-2-2008";
var date_arr:Array = date_str.split("/");
trace(isValidDate(date_arr[0],date_arr[1],date_arr[2]));
// returns false