Tuesday, December 2, 2008

Accessing HTML page details

Flash only has a basic in-built property to retrieve details of the HTML page in which an SWF is embedded. You can retrieve the URL of the SWF file itself, for example with the following code:

myTxt.text = this._url;

Later versions of the Flash player also allow you to retrieve the URL of any JPEGs, GIFs or PNG files by applying the _url property to a movieclip into which any of those filetypes has been loaded.

However, to retrieve the page URL, you have to use External Interface to communicate with the page directly. For example, the following code will retrieve the page URL:

import flash.external.ExternalInterface;
var pageURL = ExternalInterface.call('window.location.href.toString');
myTxt.text = pageURL;

A similar technique can be used to retrieve the HTML page title:

import flash.external.ExternalInterface;
var pageTitle = ExternalInterface.call('window.document.title.toString');
myTxt.text = pageTitle;

With both techniques, you can also consider using Flash's unescape function to convert any URL-encoded characters into ASCII strings:

myTxt.text = unescape(myTxt.text);

Finally, it's possible to use ExternalInterface to change details of the HTML page rather than just retrieving them. Create an FLA containing a dynamic text field and a button. Add the following code:

import flash.external.*; // imports the external interface class
var newTitle:String;
btn.onPress = function() {
newTitle = String(ExternalInterface.call("changeDocumentTitle", title_txt.text));
};

Publish the SWF file and the corresponding HTML page. Now edit the HTML page and add the following javascript code:

<SCRIPT LANGUAGE="JavaScript">
function changeDocumentTitle(a) {
if (a != null) {
window.document.title=a;
window.document.write;
}
}
</SCRIPT>

If you upload the SWF and HTML page, you should now be able to change the HTML page title dynamically from Flash. You can see a working example here and get the source files here.

Obviously you wouldn't necessarily want to use a text field to do this but, by storing newTitle as a variable, you can alter the HTML page dynamically, for example as part of a Flash navigation.

Sunday, November 30, 2008

Rounding numbers

Previous posts touched on the subject of formatting numbers; either as formatting currency or separating with commas and two decimal places. Another question that's frequently asked is how to round to different numbers of decimal places?

The standard answer is to use powers of 10 as per the following examples. To round to two decimal places use:
roundedNumber =  Math.round(yourNumber * 100)/100;

For three decimal places, use:
roundedNumber =  Math.round(yourNumber * 1000)/1000;

However, an easier way is to create a quick function to perform the work for you:

function roundDecimal(num:Number, places:Number):Number {
// defaults to two decimal places
if (places == null) {
places = 2;
}
power = Math.pow(10, places);
return Math.round(num * power) / power;
}

Use it as follows:

trace(roundDecimal(3.14159265358979, 5)); // Outputs 3.14159
trace(roundDecimal(3.14159265358979, 3)); // Outputs 3.142

Wednesday, August 6, 2008

Formatting currency

Following on from the previous entry about formatting numbers, it's a simple task to convert the same function into one that accepts currency symbols:


function formatCurrency(num:Number, comma:Boolean, currency:String):String {
// return a zero value if num is not valid
if (isNaN(num)) {
return "0.00";
}
// return a blank value if currency is not valid
if (currency == undefined) {
currency = "";
}
// round num to the nearest 100th
num = Math.round(num * 100) / 100;
// convert num to a string
var num_str:String = String(num);
// seperate any decimals from the whole numbers
var num_array = num_str.split(".");
// if there are no decimals add them using "00"
if (num_array[1] == undefined) {
num_array[1] = "00";
}
// if the decimals are too short, add an extra "0"
if (num_array[1].length == 1) {
num_array[1] += "0";
}
// separate whole numbers with commas
// if required (comma = true)
if (comma) {
var whole_array:Array = new Array();
var start:Number;
var end:Number = num_array[0].length;
while (end > 0) {
start = Math.max(end - 3, 0);
whole_array.unshift(num_array[0].slice(start, end));
end = start;
}
num_array[0] = whole_array.join(",");
}
// construct a return string joining
// the whole numbers with the decimals
// and prefixing a currency symbol
return (currency + num_array.join("."));
}
trace(formatCurrency(1234.5, true, "£"));
// outputs £1,234.50
trace(formatCurrency(1234.5, true, "\u00a5"));
// outputs ¥1,234.50

Formatting numbers

This one comes up a lot - how to format a number with commas and decimal points? Just pass your number into this function and it will return a formatted string. Including commas is optional.


function formatNumbers(num:Number, comma:Boolean):String {
// return a zero value if num is not valid
if (isNaN(num)) {
return "0.00";
}
// round num to the nearest 100th
num = Math.round(num * 100) / 100;
// convert num to a string
var num_str:String = String(num);
// seperate any decimals from the whole numbers
var num_array = num_str.split(".");
// if there are no decimals add them using "00"
if (num_array[1] == undefined) {
num_array[1] = "00";
}
// if the decimals are too short, add an extra "0"
if (num_array[1].length == 1) {
num_array[1] += "0";
}
// separate whole numbers with commas
// if required (comma = true)
if (comma) {
var whole_array:Array = new Array();
var start:Number;
var end:Number = num_array[0].length;
while (end > 0) {
start = Math.max(end - 3, 0);
whole_array.unshift(num_array[0].slice(start, end));
end = start;
}
num_array[0] = whole_array.join(",");
}
// construct a return string joining
// the whole numbers with the decimals
return (num_array.join("."));
}
trace(formatNumbers(1234));
// outputs 1234.00
trace(formatNumbers(1234.56));
// outputs 1234.56
trace(formatNumbers(1234.5, true));
// outputs 1,234.50
trace(formatNumbers(-1234.56789, true));
// outputs -1,234.57

Monday, August 4, 2008

Numbers to words in Flash

It is sometimes necessary to convert a given number into its equivalent value in words.

At first this might appear to be a daunting and complex task but, fortunately, the method for formulating spoken numbers in English can be broken down into a set of simple rules. These rules are then applicable for any number, regardless of its size:

  1. if the number value is zero then the number in words is 'zero' and no other rules apply.
  2. all numbers can be split into groups of three digits starting from the right-hand side. Each group of three digits can then be processed individually to obtain their hundreds, tens and unit word equivalents.
  3. if the hundreds portion of a three-digit group is not zero, the number of hundreds is added as a word. If the three-digit group is exactly divisible by one hundred, the text hundred is appended. If not, the text hundred and is appended, for example three hundred or one hundred and forty six.
  4. if the tens section of a three-digit group is two or higher, the appropriate -ty word (twenty, thirty, etc.) is added to the text and followed by the name of any non-zero third digit. If the tens and the units are both zero, then no text is added. For all other values, the name of the one or two-digit number is added as a special case.
  5. each group of three digits can be recombined with the addition of any relevant scale number (thousand, million, billion) separated by a comma, unless the group is blank in which case it's not included at all. The exception to this rule is when the final group of three digits does not include any hundreds and there is more than one non-blank group. In this case, the final comma is replaced with and, for example one million and forty six.
  6. negative numbers are preceded by a word to indicate this negativity, for example Minus

The following function obeys all of the above rules by, firstly, splitting any given number into groups of three digits which are stored as individual elements in an array. Secondly, it then converts each of the elements from this array into its equivalent value in words, using the rules outlined above, and stores them into a new array. Finally, it takes the new array, recombines all of the elements, and applies any additional formatting to produce the returned string:


function NumberToWords(num:Number, display:Number, minus:String):String {
if (minus == null) {
minus = "Minus";
}
var smallNumbers:Array = new Array("Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen");
var tenNumbers:Array = new Array("", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety");
var scaleNumbers:Array = new Array("", "Thousand", "Million", "Billion");
if (num == 0) {
return smallNumbers[0];
}
var digitGroups:Array = new Array();
var positive:Number = Math.abs(num);
for (var i:Number = 0; i < 4; i++) {
digitGroups[i] = Math.floor(positive % 1000);
positive /= 1000;
}
groupText = new Array();
for (var i:Number = 0; i < 4; i++) {
groupText[i] = digitGroupToWords(digitGroups[i]);
}
function digitGroupToWords(threeDigits) {
groupText[i] = "";
var hundreds:Number = Math.floor(threeDigits / 100);
var tensUnits:Number = Math.floor(threeDigits % 100);
if (hundreds != 0) {
groupText[i] += smallNumbers[hundreds] + " Hundred";
if (tensUnits != 0) {
groupText[i] += " and ";
}
}
var tens:Number = Math.floor(tensUnits / 10);
var units:Number = Math.floor(tensUnits % 10);
if (tens >= 2) {
groupText[i] += tenNumbers[tens];
if (units != 0) {
groupText[i] += " " + smallNumbers[units];
}
} else if (tensUnits != 0) {
groupText[i] += smallNumbers[tensUnits];
}
return groupText[i];
}
var combined:String = groupText[0];
var appendAnd:Boolean;
appendAnd = (digitGroups[0] > 0) && (digitGroups[0] < 100);
for (var i:Number = 1; i < 4; i++) {
if (digitGroups[i] != 0) {
var prefix:String = groupText[i] + " " + scaleNumbers[i];
if (combined.length != 0) {
prefix += appendAnd ? " and " : ", ";
}
appendAnd = false;
combined = prefix + combined;
}
}
if (num < 0) {
combined = minus + " " + combined;
}
switch (display) {
case 0 :
// Upper case
combined = combined.toUpperCase();
break;
case 1 :
// Sentence case
combined = combined.substr(0, 1) + combined.substring(1).toLowerCase();
break;
case 2 :
// Lower case
combined = combined.toLowerCase();
break;
default :
// Capitalised
break;
}
return combined;
}

The function accepts three arguments:

  1. the number to be converted into words, e.g. 474635
  2. a value representing the case of the returned string of words:
    0 = upper case, e.g. ONE HUNDRED AND EIGHT
    1 = sentence case, e.g. One hundred and eight
    2 = lower case, e.g. one hundred and eight
    3 = capitalised, e.g. One Hundred and Eight
    The default is 3
  3. the prefix to signify a negative number, e.g. Negative
    The default value is Minus
Examples of the function's usage are shown here:


// default use
trace(NumberToWords(123456789));
// lower case
trace(NumberToWords(123456789, 1));
// upper case with an alternative negative prefix
trace(NumberToWords(-123456789, 0, "negative"));

Saturday, August 2, 2008

Delay and jump to new frame

A frequent problem is how to delay the timeline, for a given period of time, and then branch to another frame once the delay is up.

A bespoke function and the setTimeout function can be combined to do this with ease:


stop();
var delay:Number = 1000;
var frameNumber:Number = 10;
function playFrame(frameNumber:Number)
{
gotoAndStop(frameNumber);
}
setTimeout(playFrame, delay, frameNumber);

delay is the length of time before the setTimeout calls the playFrame function. This is expressed in milliseconds.
frameNumber is the target frame to jump to once the delay is over.

The same principle works equally as well with frame labels:


stop();
var delay:Number = 2000;
var frameLabel:String = "menu";
function playFrame(frameLabel:String)
{
gotoAndStop(frameLabel);
}
setTimeout(playFrame, delay, frameLabel);

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