Stephen McKamey avatar Stephen McKamey committed 9d2c6b5

next iteration of fractional values (remaining issue with rounding); documentation updated

Comments (0)

Files changed (11)

 The MIT License
 
-Copyright (c) 2006-2011 Stephen M. McKamey
+Copyright (c) 2006-2012 Stephen M. McKamey
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 
 A simple but flexible API is the goal of *Countdown.js*. There is one global function with a set of static constants:
 
-    countdown(start|callback, end|callback, units);
+    var timespan = countdown(start|callback, end|callback, units, max);
 
-The parameters are a starting Date, ending Date and an optional set of units. If units is left off, it defaults to `countdown.DEFAULTS`.
+The parameters are a starting Date, ending Date, an optional set of units, and an optional maximum number of units. If units is left off, it defaults to `countdown.DEFAULTS`.
 
 	countdown.ALL =
 		countdown.MILLENNIA |
 
 This allows a very minimal call to accept the defaults and get the time since/until a single date. For example:
 
-	countdown( new Date(2000, 0, 1) );
+	countdown( new Date(2000, 0, 1) ).toString();
 
-This will toString() something like:
+This will produce a human readable description like:
 
 	11 years, 8 months, 4 days, 10 hours, 12 minutes, and 43 seconds
 
-### Timespan result
-
-The return value is a Timespan object which always contains the following fields:
-
-- `Date start`: the starting date object used for the calculation
-- `Date end`: the ending date object used for the calculation
-- `Number units`: the units specified
-- `Number value`: total milliseconds difference (i.e., end - start) if end < start this will be negative
-
-Typically the `end` occurs after `start` but the arguments were reversed, the only difference is `Timespan.value` will be negative. The sign of `value` can be used to determine if the event occurs in the future or in the past. 
-
-The following time unit fields are only present if their corresponding units were requested:
-
-- `Number millennia`
-- `Number centuries`
-- `Number decades`
-- `Number years`
-- `Number months`
-- `Number days`
-- `Number hours`
-- `Number minutes`
-- `Number seconds`
-- `Number milliseconds`
-
-Finally, Timespan has a few formatting methods:
-
-`toString()`: formats the Timespan object as an English sentence, e.g.,
-
-	ts.toString() => "5 years, 1 month, 19 days, 12 hours, and 17 minutes"
-
-`toString(max)`: formats the Timespan object as an English sentence, but only returns the `max` most significant units. e.g., using the same input:
-
-	toString(2) => "5 years, and 1 month
-
-Negative values of `max` indicates the number of units to leave off. e.g., using the same input:
-
-	toString(-2) => "5 years, 1 month, and 19 days"
-
-`toHTML(tagName)`: formats the Timespan object as an English sentence, with the specified HTML tag wrapped around each unit. If no tag name is provided, "`span`" is used. e.g. using the same input,
-
-	ts.toHTML("em") => "<em>5 years</em>, <em>1 month</em>, <em>19 days</em>, <em>12 hours</em>, and <em>17 minutes</em>"
-
-`toHTML(tagName, max)`: the optional `max` parameter similarly restricts the total number of units returned. e.g., using the same input:
-
-	ts.toHTML("em", 3) => "<em>5 years</em>, <em>1 month</em>, and <em>19 days</em>"
-	ts.toHTML("em", -1) => "<em>5 years</em>, <em>1 month</em>, <em>19 days</em>, and <em>12 hours</em>"
-
-If `start` and `end` are exactly the same (for the requested granularity of units), or `max` is zero, then `toString()` and `toHTML()` will return an empty string.
-
 ### The `start` / `end` arguments
 
 The parameters `start` and `end` can be one of several values:
 
 ### The `units` argument
 
-The static units constants can be combined using standard bitwise operators. For example, to explicitly include "months and years" use bitwise-OR:
+The static units constants can be combined using [standard bitwise operators](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/Bitwise_Operators). For example, to explicitly include "months or days" use bitwise-OR:
 
 	countdown.MONTHS | countdown.DAYS
 
 
 	~countdown.WEEKS & ~countdown.MILLISECONDS
 
-Equivalently, to specify everything but "not weeks or milliseconds" wrap bitwise-NOT around bitwise-OR:
+[Equivalently](http://en.wikipedia.org/wiki/De_Morgan's_laws), to specify everything but "not weeks or milliseconds" wrap bitwise-NOT around bitwise-OR:
 
 	~(countdown.WEEKS | countdown.MILLISECONDS)
 
+### The `max` argument
+
+#### Breaking change for v2.3.0!
+The `max` argument used to be specified in `.toString(...)` and `.toHTML(...)`. v2.3.0 moves it to `countdown(...)`, which improves efficiency as well as enabling fractional units (see below).
+
+The final optional argument specifies a maximum number of unit labels to display. This allows specifying which units are interesting but only displaying the `max` most significant units.
+
+	countdown(start, end, units).toString() => "5 years, 1 month, 19 days, 12 hours, and 17 minutes"
+
+Specifying `max` as `2` ensures that only the two most significant units are displayed **(note the rounding of months)**:
+
+	countdown(start, end, units, 2).toString() => "5 years, and 2 months"
+
+Negative or zero values of `max` are ignored.
+
+### Timespan result
+
+The return value is a Timespan object which always contains the following fields:
+
+- `Date start`: the starting date object used for the calculation
+- `Date end`: the ending date object used for the calculation
+- `Number units`: the units specified
+- `Number value`: total milliseconds difference (i.e., `end` - `start`). If `end` < `start` then `value` will be negative.
+
+Typically the `end` occurs after `start`, but if the arguments were reversed, the only difference is `Timespan.value` will be negative. The sign of `value` can be used to determine if the event occurs in the future or in the past. 
+
+The following time unit fields are only present if their corresponding units were requested:
+
+- `Number millennia`
+- `Number centuries`
+- `Number decades`
+- `Number years`
+- `Number months`
+- `Number days`
+- `Number hours`
+- `Number minutes`
+- `Number seconds`
+- `Number milliseconds`
+
+Finally, Timespan has two formatting methods each with some optional parameters:
+
+`String toString(digits)`: formats the Timespan object as an English sentence. The optional `digits` argument allows fractional values on the smallest unit. e.g., using the same input
+
+	ts.toString() => "5 years, and 2 months"
+	ts.toString(2) => "5 years, and 1.65 months"
+
+`String toHTML(tagName, digits)`: formats the Timespan object as an English sentence, with the specified HTML tag wrapped around each unit. If no tag name is provided, "`span`" is used. Again, the optional `digits` argument restricts the total number of units returned. e.g., using the same input:
+
+	ts.toHTML("em") => "<em>5 years</em>, <em>1 month</em>, <em>19 days</em>, <em>12 hours</em>, and <em>17 minutes</em>"
+	ts.toHTML("em", 3) => "<em>5 years</em>, <em>1 month</em>, <em>19 days</em>, <em>12 hours</em>, and <em>17.193 minutes</em>"
+
+Digits must be between `0` and `20`, inclusive. Negative values of `digits` are ignored.
+
+If `start` and `end` are exactly the same or the difference is below the requested granularity of units, then `toString()` and `toHTML()` will simply return an empty string.
+
+#### Rounding
+With the calculations of fractional units in v2.3.0, the smallest displayed unit now properly rounds. Previously, the equivalent of `1.99 years` would be truncated to `1 year`, as of v2.3.0 it will display as `2 years`.  
+Typically, this is the intended interpretation but there are a few circumstances where people expect the truncated behavior. For example, people often talk about their age as the lowest possible interpretation. e.g., they claim "39-years-old" right up until the morning of their 40th birthday (some people do even for years after!). In these cases, after calling `countdown(...)`, you might want to set `ts.years = Math.floor(ts.years)` before calling `ts.toString(0)`. The vain might want you to set `ts.years = Math.min(ts.years, 39)`!
+
+#### Breaking change for v2.3.0!
+Previously, the `max` number of unit labels used to be defined when formatting. Now it is specified with the units themselves (see above).
+
 ## License
 
 Distributed under the terms of [The MIT license][2].
 		var a = ref.getTime();
 
 		// increment month by 1
-		var b = new Date(a).setUTCMonth( ref.getUTCMonth() + 1 ).getTime();
+		var b = new Date(a);
+		b.setUTCMonth( ref.getUTCMonth() + 1 );
 
 		// this is the trickiest since months vary in length
-		return Math.round( (b - a) / MILLISECONDS_PER_DAY );
+		return Math.round( (b.getTime() - a) / MILLISECONDS_PER_DAY );
 	}
 
 	/**
 		var a = ref.getTime();
 
 		// increment year by 1
-		var b = new Date(a).setUTCFullYear( ref.getUTCFullYear() + 1 ).getTime();
+		var b = new Date(a);
+		b.setUTCFullYear( ref.getUTCFullYear() + 1 );
 
 		// this is the trickiest since years (periodically) vary in length
-		return Math.round( (b - a) / MILLISECONDS_PER_DAY );
+		return Math.round( (b.getTime() - a) / MILLISECONDS_PER_DAY );
 	}
 
 	/**
 	 * @private
 	 * @param {number} value number to format
-	 * @param {number} digits number of digits right of decimal point
+	 * @param {number} digits number of digits right of decimal point [0-20]
 	 * @return {number}
 	 */
 	function maxDigits(value, digits) {
 		// ensure does not have more than specified number of digits
-		return (+(+value).toFixed(+digits || 0));
+		return (+(+value).toFixed(digits));
 	}
 
 	/**
 	 * @param {number} value
 	 * @param {string} singular
 	 * @param {string} plural
-	 * @param {number} digits
+	 * @param {number} digits number of digits right of decimal point [0-20]
 	 * @return {string}
 	 */
 	function plurality(value, singular, plural, digits) {
 	 * 
 	 * @private
 	 * @param {Timespan} ts
-	 * @param {number} max number of labels to output
 	 * @param {number} digits max number of decimal digits to output
 	 * @return {Array}
 	 */
 	 * Formats the Timespan as a sentance
 	 * 
 	 * @private
-	 * @param {number} max number of labels to output
 	 * @param {number} digits max number of decimal digits to output
 	 * @return {string}
 	 */
-	Timespan.prototype.toString = function(max, digits) {
-		var label = formatList(this, max, digits);
+	Timespan.prototype.toString = function(digits) {
+		var label = formatList(this, digits);
 
 		var count = label.length;
 		if (!count) {
 	 * 
 	 * @private
 	 * @param {string} tag HTML tag name to wrap each value
-	 * @param {number} max number of labels to output
 	 * @param {number} digits max number of decimal digits to output
 	 * @return {string}
 	 */
-	Timespan.prototype.toHTML = function(tag, max, digits) {
+	Timespan.prototype.toHTML = function(tag, digits) {
 		tag = tag || 'span';
-		var label = formatList(this, max, digits);
+		var label = formatList(this, digits);
 
 		var count = label.length;
 		if (!count) {
 	};
 
 	/**
-	 * Ripple up partial units
+	 * Ripple up partial units one place
+	 * 
+	 * @private
+	 * @param {Timespan} ts timespan
+	 * @param {number} frac accumulated fractional value
+	 * @param {string} fromUnit source unit name
+	 * @param {string} toUnit target unit name
+	 * @param {number} conversion multiplier between units
+	 * @return {number} new fractional value
+	 */
+	function fraction(ts, frac, fromUnit, toUnit, conversion) {
+		if (ts[fromUnit] >= 0) {
+			frac += ts[fromUnit];
+			delete ts[fromUnit];
+		}
+
+		frac /= conversion;
+		if (frac + 1 <= 1) {
+			// drop if below machine epsilon
+			return 0;
+		}
+
+		if (ts[toUnit] >= 0) {
+			ts[toUnit] += frac;
+			return 0;
+		}
+
+		return frac;
+	}
+
+	/**
+	 * Ripple up partial units to next existing
 	 * 
 	 * @private
 	 * @param {Timespan} ts
 	 */
-	function fraction(ts) {
-		var frac = ts.milliseconds / MILLISECONDS_PER_SECOND;
-		delete ts.milliseconds;
+	function fractional(ts) {
+		var frac = fraction(ts, 0, 'milliseconds', 'seconds', MILLISECONDS_PER_SECOND);
+		if (!frac) { return; }
 
-		if (ts.seconds) {
-			ts.seconds += frac;
-			return;
-		}
+		frac = fraction(ts, frac, 'seconds', 'minutes', SECONDS_PER_MINUTE);
+		if (!frac) { return; }
 
-		frac /= SECONDS_PER_MINUTE;
-		if (frac + 1 <= 1) {
-			// shortcut if below epsilon
-			return;
-		}
+		frac = fraction(ts, frac, 'minutes', 'hours', MINUTES_PER_HOUR);
+		if (!frac) { return; }
 
-		if (ts.minutes) {
-			ts.minutes += frac;
-			return;
-		}
+		frac = fraction(ts, frac, 'hours', 'days', HOURS_PER_DAY);
+		if (!frac) { return; }
 
-		frac /= MINUTES_PER_HOUR;
-		if (frac + 1 <= 1) {
-			// shortcut if below epsilon
-			return;
-		}
+		frac = fraction(ts, frac, 'days', 'weeks', DAYS_PER_WEEK);
+		if (!frac) { return; }
 
-		if (ts.hours) {
-			ts.hours += frac;
-			return;
-		}
+		frac = fraction(ts, frac, 'weeks', 'months', daysPerMonth(ts.refMonth)/DAYS_PER_WEEK);
+		if (!frac) { return; }
 
-		frac /= HOURS_PER_DAY;
-		if (frac + 1 <= 1) {
-			// shortcut if below epsilon
-			return;
-		}
+		frac = fraction(ts, frac, 'months', 'years', daysPerYear(ts.refMonth)/daysPerMonth(ts.refMonth));
+		if (!frac) { return; }
 
-		if (ts.days) {
-			ts.days += frac;
-			return;
-		}
+		frac = fraction(ts, frac, 'years', 'decades', YEARS_PER_DECADE);
+		if (!frac) { return; }
 
-		if (ts.weeks) {
-			// only convert if needed here
-			// as months are problematic
-			frac /= DAYS_PER_WEEK;
-			// no need to shortcut
-			ts.weeks += frac;
-			return;
-		}
+		frac = fraction(ts, frac, 'decades', 'centuries', DECADES_PER_CENTURY);
+		if (!frac) { return; }
 
-		if (ts.months) {
-			frac /= daysPerMonth(ts.refMonth);
-			// no need to shortcut
-			ts.months += frac;
-			return;
-		}
+		frac = fraction(ts, frac, 'centuries', 'millennia', CENTURIES_PER_MILLENNIUM);
 
-		frac /= daysPerYear(ts.refMonth);
-		if (frac + 1 <= 1) {
-			// shortcut if below epsilon
-			return;
-		}
-
-		if (ts.years) {
-			ts.years += frac;
-			return;
-		}
-
-		frac /= YEARS_PER_DECADE;
-		if (frac + 1 <= 1) {
-			// shortcut if below epsilon
-			return;
-		}
-
-		if (ts.decades) {
-			ts.decades += frac;
-			return;
-		}
-
-		frac /= DECADES_PER_CENTURY;
-		if (frac + 1 <= 1) {
-			// shortcut if below epsilon
-			return;
-		}
-
-		if (ts.centuries) {
-			ts.centuries += frac;
-			return;
-		}
-
-		frac /= CENTURIES_PER_MILLENNIUM;
-		if (frac + 1 <= 1) {
-			// shortcut if below epsilon
-			return;
-		}
-
-		if (ts.millennia) {
-			ts.millennia += frac;
-			return;
-		}
+		// should never reach this with remaining fractional value
+		if (frac) { throw new Error('Fractional unit overflow'); }
 	}
 
 	/**
 	 * 
 	 * @private
 	 * @param {Timespan} ts
-	 * @param {number} max number of labels to output
 	 * @param {number} digits max number of decimal digits to output
 	 * @return {Array}
 	 */
-	formatList = function(ts, max, digits) {
+	formatList = function(ts, digits) {
+		// clamp digits to an integer between [0, 20]
+		digits = (digits > 0) ? (digits < 20) ? Math.round(digits) : 20 : 0;
+
 		var list = [];
 
-		if (ts.millennia) {
+		if (digits ? ts.millennia : (ts.millennia >= 1)) {
 			list.push(plurality(ts.millennia, 'millennium', 'millennia', digits));
-			if (list.length === max) { return list; }
 		}
-		if (ts.centuries) {
+		if (digits ? ts.centuries : (ts.centuries >= 1)) {
 			list.push(plurality(ts.centuries, 'century', 'centuries', digits));
-			if (list.length === max) { return list; }
 		}
-		if (ts.decades) {
+		if (digits ? ts.decades : (ts.decades >= 1)) {
 			list.push(plurality(ts.decades, 'decade', 'decades', digits));
-			if (list.length === max) { return list; }
 		}
-		if (ts.years) {
+		if (digits ? ts.years : (ts.years >= 1)) {
 			list.push(plurality(ts.years, 'year', 'years', digits));
-			if (list.length === max) { return list; }
 		}
-		if (ts.months) {
+		if (digits ? ts.months : (ts.months >= 1)) {
 			list.push(plurality(ts.months, 'month', 'months', digits));
-			if (list.length === max) { return list; }
 		}
-		if (ts.weeks) {
+		if (digits ? ts.weeks : (ts.weeks >= 1)) {
 			list.push(plurality(ts.weeks, 'week', 'weeks', digits));
-			if (list.length === max) { return list; }
 		}
-		if (ts.days) {
+		if (digits ? ts.days : (ts.days >= 1)) {
 			list.push(plurality(ts.days, 'day', 'days', digits));
-			if (list.length === max) { return list; }
 		}
-		if (ts.hours) {
+		if (digits ? ts.hours : (ts.hours >= 1)) {
 			list.push(plurality(ts.hours, 'hour', 'hours', digits));
-			if (list.length === max) { return list; }
 		}
-		if (ts.minutes) {
+		if (digits ? ts.minutes : (ts.minutes >= 1)) {
 			list.push(plurality(ts.minutes, 'minute', 'minutes', digits));
-			if (list.length === max) { return list; }
 		}
-		if (ts.seconds) {
+		if (digits ? ts.seconds : (ts.seconds >= 1)) {
 			list.push(plurality(ts.seconds, 'second', 'seconds', digits));
-			if (list.length === max) { return list; }
 		}
-		if (ts.milliseconds) {
+		if (digits ? ts.milliseconds : (ts.milliseconds >= 1)) {
 			list.push(plurality(ts.milliseconds, 'millisecond', 'milliseconds', digits));
-			if (list.length === max) { return list; }
 		}
 
 		return list;
 	 * @private
 	 * @param {Timespan} ts
 	 * @param {number} units the units to populate
+	 * @param {number} max number of labels to output
 	 */
-	function pruneUnits(ts, units) {
+	function pruneUnits(ts, units, max) {
+		max = (max > 0) ? max : NaN;
+		var count = 0;
+
 		// Calc from largest unit to smallest to prevent underflow
-
-		if (!(units & MILLENNIA)) {
+		if (!(units & MILLENNIA) || (count >= max)) {
 			// ripple millennia down to centuries
 			ts.centuries += ts.millennia * CENTURIES_PER_MILLENNIUM;
 			delete ts.millennia;
+
+		} else if (ts.millennia) {
+			count++;
 		}
 
-		if (!(units & CENTURIES)) {
+		if (!(units & CENTURIES) || (count >= max)) {
 			// ripple centuries down to decades
 			ts.decades += ts.centuries * DECADES_PER_CENTURY;
 			delete ts.centuries;
+
+		} else if (ts.centuries) {
+			count++;
 		}
 
-		if (!(units & DECADES)) {
+		if (!(units & DECADES) || (count >= max)) {
 			// ripple decades down to years
 			ts.years += ts.decades * YEARS_PER_DECADE;
 			delete ts.decades;
+
+		} else if (ts.decades) {
+			count++;
 		}
 
-		if (!(units & YEARS)) {
+		if (!(units & YEARS) || (count >= max)) {
 			// ripple years down to months
 			ts.months += ts.years * MONTHS_PER_YEAR;
 			delete ts.years;
+
+		} else if (ts.years) {
+			count++;
 		}
 
-		if (!(units & MONTHS) && ts.months) {
+		if (!(units & MONTHS) || (count >= max)) {
 			// ripple months down to days
-			ts.days += borrowMonths(ts.refMonth, ts.months);
+			if (ts.months) {
+				ts.days += borrowMonths(ts.refMonth, ts.months);
+			}
 			delete ts.months;
 
 			if (ts.days >= DAYS_PER_WEEK) {
 				ts.weeks += floor(ts.days / DAYS_PER_WEEK);
 				ts.days %= DAYS_PER_WEEK;
 			}
+
+		} else if (ts.months) {
+			count++;
 		}
 
-		if (!(units & WEEKS)) {
+		if (!(units & WEEKS) || (count >= max)) {
 			// ripple weeks down to days
 			ts.days += ts.weeks * DAYS_PER_WEEK;
 			delete ts.weeks;
+
+		} else if (ts.weeks) {
+			count++;
 		}
 
-		if (!(units & DAYS)) {
+		if (!(units & DAYS) || (count >= max)) {
 			//ripple days down to hours
 			ts.hours += ts.days * HOURS_PER_DAY;
 			delete ts.days;
+
+		} else if (ts.days) {
+			count++;
 		}
 
-		if (!(units & HOURS)) {
+		if (!(units & HOURS) || (count >= max)) {
 			// ripple hours down to minutes
 			ts.minutes += ts.hours * MINUTES_PER_HOUR;
 			delete ts.hours;
+
+		} else if (ts.hours) {
+			count++;
 		}
 
-		if (!(units & MINUTES)) {
+		if (!(units & MINUTES) || (count >= max)) {
 			// ripple minutes down to seconds
 			ts.seconds += ts.minutes * SECONDS_PER_MINUTE;
 			delete ts.minutes;
+
+		} else if (ts.minutes) {
+			count++;
 		}
 
-		if (!(units & SECONDS)) {
+		if (!(units & SECONDS) || (count >= max)) {
 			// ripple seconds down to milliseconds
 			ts.milliseconds += ts.seconds * MILLISECONDS_PER_SECOND;
 			delete ts.seconds;
+
+		} else if (ts.seconds) {
+			count++;
 		}
 
 		// nothing to ripple milliseconds down to
 		// so ripple back up to smallest existing unit as a fractional value
-		if (!(units & MILLISECONDS)) {
-			fraction(ts);
+		if (!(units & MILLISECONDS) || (count >= max)) {
+			fractional(ts);
 		}
 	}
 
 	 * @param {Date} end the ending date
 	 * @param {number} units the units to populate
 	 */
-	function populate(ts, start, end, units) {
+	function populate(ts, start, end, units, max) {
 		ts.start = start;
 		ts.end = end;
 		ts.units = units;
 			ts.milliseconds = end.getUTCMilliseconds() - start.getUTCMilliseconds();
 
 			ripple(ts);
-			pruneUnits(ts, units);
+			pruneUnits(ts, units, max);
 
 		} finally {
 			delete ts.refMonth;
 	 * @param {Date|number|null|function(Timespan)} start the starting date
 	 * @param {Date|number|null|function(Timespan)} end the ending date
 	 * @param {number} units the units to populate
+	 * @param {number} max number of labels to output
 	 * @return {Timespan|number}
 	 */
-	function countdown(start, end, units) {
+	function countdown(start, end, units, max) {
 		var callback;
 
 		// ensure some units or use defaults
 		}
 
 		if (!callback) {
-			return populate(new Timespan(), /** @type{Date} */(start||new Date()), /** @type{Date} */(end||new Date()), units);
+			return populate(new Timespan(), /** @type{Date} */(start||new Date()), /** @type{Date} */(end||new Date()), units, max);
 		}
 
 		// base delay off units
 		var delay = getDelay(units);
 		var fn = function() {
 			callback(
-				populate(new Timespan(), /** @type{Date} */(start||new Date()), /** @type{Date} */(end||new Date()), units)
+				populate(new Timespan(), /** @type{Date} */(start||new Date()), /** @type{Date} */(end||new Date()), units, max)
 			);
 		};
 
  Copyright (c)2006-2012 Stephen M. McKamey.
  Licensed under The MIT License.
 */
-var module,countdown=function(k){function o(a,c){var d=a.getTime();a.setUTCMonth(a.getUTCMonth()+c);return Math.round((a.getTime()-d)/864E5)}function h(a,c,d,b){a=+(+a).toFixed(+b||0);return a+" "+(1===a?c:d)}function j(){}function l(a,c,d,b){a.start=c;a.end=d;a.units=b;a.value=d.getTime()-c.getTime();if(0>a.value)var f=d,d=c,c=f;a.refMonth=new Date(c.getFullYear(),c.getMonth(),15);try{a.millennia=0;a.centuries=0;a.decades=0;a.years=d.getUTCFullYear()-c.getUTCFullYear();a.months=d.getUTCMonth()-c.getUTCMonth();
-a.weeks=0;a.days=d.getUTCDate()-c.getUTCDate();a.hours=d.getUTCHours()-c.getUTCHours();a.minutes=d.getUTCMinutes()-c.getUTCMinutes();a.seconds=d.getUTCSeconds()-c.getUTCSeconds();a.milliseconds=d.getUTCMilliseconds()-c.getUTCMilliseconds();var g;0>a.milliseconds?(g=m(-a.milliseconds/1E3),a.seconds-=g,a.milliseconds+=1E3*g):1E3<=a.milliseconds&&(a.seconds+=i(a.milliseconds/1E3),a.milliseconds%=1E3);0>a.seconds?(g=m(-a.seconds/60),a.minutes-=g,a.seconds+=60*g):60<=a.seconds&&(a.minutes+=i(a.seconds/
-60),a.seconds%=60);0>a.minutes?(g=m(-a.minutes/60),a.hours-=g,a.minutes+=60*g):60<=a.minutes&&(a.hours+=i(a.minutes/60),a.minutes%=60);0>a.hours?(g=m(-a.hours/24),a.days-=g,a.hours+=24*g):24<=a.hours&&(a.days+=i(a.hours/24),a.hours%=24);for(;0>a.days;)a.months--,a.days+=o(a.refMonth,1);7<=a.days&&(a.weeks+=i(a.days/7),a.days%=7);0>a.months?(g=m(-a.months/12),a.years-=g,a.months+=12*g):12<=a.months&&(a.years+=i(a.months/12),a.months%=12);10<=a.years&&(a.decades+=i(a.years/10),a.years%=10,10<=a.decades&&
-(a.centuries+=i(a.decades/10),a.decades%=10,10<=a.centuries&&(a.millennia+=i(a.centuries/10),a.centuries%=10)));b&1024||(a.centuries+=10*a.millennia,delete a.millennia);b&512||(a.decades+=10*a.centuries,delete a.centuries);b&256||(a.years+=10*a.decades,delete a.decades);b&128||(a.months+=12*a.years,delete a.years);!(b&64)&&a.months&&(a.days+=o(a.refMonth,a.months),delete a.months,7<=a.days&&(a.weeks+=i(a.days/7),a.days%=7));b&32||(a.days+=7*a.weeks,delete a.weeks);b&16||(a.hours+=24*a.days,delete a.days);
-b&8||(a.minutes+=60*a.hours,delete a.hours);b&4||(a.seconds+=60*a.minutes,delete a.minutes);b&2||(a.milliseconds+=1E3*a.seconds,delete a.seconds);if(!(b&1)){var e=a.milliseconds/1E3;delete a.milliseconds;if(a.seconds)a.seconds+=e;else if(e/=60,!(1>=e+1))if(a.minutes)a.minutes+=e;else if(e/=60,!(1>=e+1))if(a.hours)a.hours+=e;else if(e/=24,!(1>=e+1))if(a.days)a.days+=e;else if(a.weeks)a.weeks+=e/7;else if(a.months){var h,j=a.refMonth,k=j.getTime(),n=(new Date(k)).setUTCMonth(j.getUTCMonth()+1).getTime();
-h=Math.round((n-k)/864E5);a.months+=e/h}else{h=e;var l,p=a.refMonth,q=p.getTime(),r=(new Date(q)).setUTCFullYear(p.getUTCFullYear()+1).getTime();l=Math.round((r-q)/864E5);e=h/l;1>=e+1||(a.years?a.years+=e:(e/=10,1>=e+1||(a.decades?a.decades+=e:(e/=10,1>=e+1||(a.centuries?a.centuries+=e:(e/=10,!(1>=e+1)&&a.millennia&&(a.millennia+=e)))))))}}}finally{delete a.refMonth}return a}function f(a,c,d){var b,d=d||222;"function"===typeof a?(b=a,a=null):a instanceof Date||(a=null!==a&&isFinite(a)?new Date(a):
-null);"function"===typeof c?(b=c,c=null):c instanceof Date||(c=null!==c&&isFinite(c)?new Date(c):null);if(!a&&!c)return new j;if(!b)return l(new j,a||new Date,c||new Date,d);var f;f=d&1?1E3/30:d&2?1E3:d&4?6E4:d&8?36E5:d&16?864E5:6048E5;var g=function(){b(l(new j,a||new Date,c||new Date,d))};g();return setInterval(g,f)}var m=Math.ceil,i=Math.floor,n;j.prototype.toString=function(a,c){var d=n(this,a,c),b=d.length;if(!b)return"";1<b&&(d[b-1]="and "+d[b-1]);return d.join(", ")};j.prototype.toHTML=function(a,
-c,d){a=a||"span";c=n(this,c,d);d=c.length;if(!d)return"";for(var b=0;b<d;b++)c[b]="<"+a+">"+c[b]+"</"+a+">";--d&&(c[d]="and "+c[d]);return c.join(", ")};n=function(a,c,d){var b=[];if(a.millennia&&(b.push(h(a.millennia,"millennium","millennia",d)),b.length===c)||a.centuries&&(b.push(h(a.centuries,"century","centuries",d)),b.length===c))return b;if(a.decades&&(b.push(h(a.decades,"decade","decades",d)),b.length===c))return b;if(a.years&&(b.push(h(a.years,"year","years",d)),b.length===c))return b;if(a.months&&
-(b.push(h(a.months,"month","months",d)),b.length===c))return b;if(a.weeks&&(b.push(h(a.weeks,"week","weeks",d)),b.length===c))return b;if(a.days&&(b.push(h(a.days,"day","days",d)),b.length===c))return b;if(a.hours&&(b.push(h(a.hours,"hour","hours",d)),b.length===c))return b;if(a.minutes&&(b.push(h(a.minutes,"minute","minutes",d)),b.length===c))return b;if(a.seconds&&(b.push(h(a.seconds,"second","seconds",d)),b.length===c))return b;a.milliseconds&&b.push(h(a.milliseconds,"millisecond","milliseconds",
-d));return b};f.MILLISECONDS=1;f.SECONDS=2;f.MINUTES=4;f.HOURS=8;f.DAYS=16;f.WEEKS=32;f.MONTHS=64;f.YEARS=128;f.DECADES=256;f.CENTURIES=512;f.MILLENNIA=1024;f.DEFAULTS=222;f.ALL=2047;k&&k.exports&&(k.exports=f);return f}(module);
+var module,countdown=function(l){function p(a,b){var c=a.getTime();a.setUTCMonth(a.getUTCMonth()+b);return Math.round((a.getTime()-c)/864E5)}function q(a){var b=a.getTime(),c=new Date(b);c.setUTCMonth(a.getUTCMonth()+1);return Math.round((c.getTime()-b)/864E5)}function h(a,b,c,d){a=+(+a).toFixed(d);return a+" "+(1===a?b:c)}function k(){}function i(a,b,c,d,e){0<=a[c]&&(b+=a[c],delete a[c]);b/=e;if(1>=b+1)return 0;return 0<=a[d]?(a[d]+=b,0):b}function n(a,b,c,d,e){a.start=b;a.end=c;a.units=d;a.value=
+c.getTime()-b.getTime();if(0>a.value)var h=c,c=b,b=h;a.refMonth=new Date(b.getFullYear(),b.getMonth(),15);try{a.millennia=0;a.centuries=0;a.decades=0;a.years=c.getUTCFullYear()-b.getUTCFullYear();a.months=c.getUTCMonth()-b.getUTCMonth();a.weeks=0;a.days=c.getUTCDate()-b.getUTCDate();a.hours=c.getUTCHours()-b.getUTCHours();a.minutes=c.getUTCMinutes()-b.getUTCMinutes();a.seconds=c.getUTCSeconds()-b.getUTCSeconds();a.milliseconds=c.getUTCMilliseconds()-b.getUTCMilliseconds();var g;0>a.milliseconds?(g=
+o(-a.milliseconds/1E3),a.seconds-=g,a.milliseconds+=1E3*g):1E3<=a.milliseconds&&(a.seconds+=j(a.milliseconds/1E3),a.milliseconds%=1E3);0>a.seconds?(g=o(-a.seconds/60),a.minutes-=g,a.seconds+=60*g):60<=a.seconds&&(a.minutes+=j(a.seconds/60),a.seconds%=60);0>a.minutes?(g=o(-a.minutes/60),a.hours-=g,a.minutes+=60*g):60<=a.minutes&&(a.hours+=j(a.minutes/60),a.minutes%=60);0>a.hours?(g=o(-a.hours/24),a.days-=g,a.hours+=24*g):24<=a.hours&&(a.days+=j(a.hours/24),a.hours%=24);for(;0>a.days;)a.months--,a.days+=
+p(a.refMonth,1);7<=a.days&&(a.weeks+=j(a.days/7),a.days%=7);0>a.months?(g=o(-a.months/12),a.years-=g,a.months+=12*g):12<=a.months&&(a.years+=j(a.months/12),a.months%=12);10<=a.years&&(a.decades+=j(a.years/10),a.years%=10,10<=a.decades&&(a.centuries+=j(a.decades/10),a.decades%=10,10<=a.centuries&&(a.millennia+=j(a.centuries/10),a.centuries%=10)));b=e;b=0<b?b:NaN;c=0;!(d&1024)||c>=b?(a.centuries+=10*a.millennia,delete a.millennia):a.millennia&&c++;!(d&512)||c>=b?(a.decades+=10*a.centuries,delete a.centuries):
+a.centuries&&c++;!(d&256)||c>=b?(a.years+=10*a.decades,delete a.decades):a.decades&&c++;!(d&128)||c>=b?(a.months+=12*a.years,delete a.years):a.years&&c++;!(d&64)||c>=b?(a.months&&(a.days+=p(a.refMonth,a.months)),delete a.months,7<=a.days&&(a.weeks+=j(a.days/7),a.days%=7)):a.months&&c++;!(d&32)||c>=b?(a.days+=7*a.weeks,delete a.weeks):a.weeks&&c++;!(d&16)||c>=b?(a.hours+=24*a.days,delete a.days):a.days&&c++;!(d&8)||c>=b?(a.minutes+=60*a.hours,delete a.hours):a.hours&&c++;!(d&4)||c>=b?(a.seconds+=60*
+a.minutes,delete a.minutes):a.minutes&&c++;!(d&2)||c>=b?(a.milliseconds+=1E3*a.seconds,delete a.seconds):a.seconds&&c++;if(!(d&1)||c>=b){var f=i(a,0,"milliseconds","seconds",1E3);if(f&&(f=i(a,f,"seconds","minutes",60)))if(f=i(a,f,"minutes","hours",60))if(f=i(a,f,"hours","days",24))if(f=i(a,f,"days","weeks",7))if(f=i(a,f,"weeks","months",q(a.refMonth)/7)){var d=f,k,l=a.refMonth,m=l.getTime(),n=new Date(m);n.setUTCFullYear(l.getUTCFullYear()+1);k=Math.round((n.getTime()-m)/864E5);if(f=i(a,d,"months",
+"years",k/q(a.refMonth)))if(f=i(a,f,"years","decades",10))if(f=i(a,f,"decades","centuries",10))if(f=i(a,f,"centuries","millennia",10))throw Error("Fractional unit overflow");}}}finally{delete a.refMonth}return a}function e(a,b,c,d){var e,c=c||222;"function"===typeof a?(e=a,a=null):a instanceof Date||(a=null!==a&&isFinite(a)?new Date(a):null);"function"===typeof b?(e=b,b=null):b instanceof Date||(b=null!==b&&isFinite(b)?new Date(b):null);if(!a&&!b)return new k;if(!e)return n(new k,a||new Date,b||new Date,
+c,d);var h;h=c&1?1E3/30:c&2?1E3:c&4?6E4:c&8?36E5:c&16?864E5:6048E5;var g=function(){e(n(new k,a||new Date,b||new Date,c,d))};g();return setInterval(g,h)}var o=Math.ceil,j=Math.floor,m;k.prototype.toString=function(a){var a=m(this,a),b=a.length;if(!b)return"";1<b&&(a[b-1]="and "+a[b-1]);return a.join(", ")};k.prototype.toHTML=function(a,b){var a=a||"span",c=m(this,b),d=c.length;if(!d)return"";for(var e=0;e<d;e++)c[e]="<"+a+">"+c[e]+"</"+a+">";--d&&(c[d]="and "+c[d]);return c.join(", ")};m=function(a,
+b){var b=0<b?20>b?Math.round(b):20:0,c=[];(b?a.millennia:1<=a.millennia)&&c.push(h(a.millennia,"millennium","millennia",b));(b?a.centuries:1<=a.centuries)&&c.push(h(a.centuries,"century","centuries",b));(b?a.decades:1<=a.decades)&&c.push(h(a.decades,"decade","decades",b));(b?a.years:1<=a.years)&&c.push(h(a.years,"year","years",b));(b?a.months:1<=a.months)&&c.push(h(a.months,"month","months",b));(b?a.weeks:1<=a.weeks)&&c.push(h(a.weeks,"week","weeks",b));(b?a.days:1<=a.days)&&c.push(h(a.days,"day",
+"days",b));(b?a.hours:1<=a.hours)&&c.push(h(a.hours,"hour","hours",b));(b?a.minutes:1<=a.minutes)&&c.push(h(a.minutes,"minute","minutes",b));(b?a.seconds:1<=a.seconds)&&c.push(h(a.seconds,"second","seconds",b));(b?a.milliseconds:1<=a.milliseconds)&&c.push(h(a.milliseconds,"millisecond","milliseconds",b));return c};e.MILLISECONDS=1;e.SECONDS=2;e.MINUTES=4;e.HOURS=8;e.DAYS=16;e.WEEKS=32;e.MONTHS=64;e.YEARS=128;e.DECADES=256;e.CENTURIES=512;e.MILLENNIA=1024;e.DEFAULTS=222;e.ALL=2047;l&&l.exports&&(l.exports=
+e);return e}(module);
 			</span><span>
 				<label for="empty-label">empty</label><input id="empty-label" type="text" value="now!" />
 			</span><span>
-				<label for="max-units">max</label><input id="max-units" type="number" min="-11" max="11" value="11" />
+				<label for="max-units">max</label><input id="max-units" type="number" min="0" max="11" value="11" />
+				<label for="frac-digits">digits</label><input id="frac-digits" type="number" min="0" max="20" value="0" />
 			<span></fieldset>
 		</form>
 
 
 	<footer>
 		<div style="float:left"><a href="http://twitter.com/share" class="twitter-share-button" data-url="http://countdownjs.org" data-text="Countdown.js: A simple JavaScript API for producing an accurate, intuitive description of the timespan between dates"></a></div>
-		Copyright &copy; 2006-2011 <a href="http://mck.me">Stephen M. McKamey</a>
+		Copyright &copy; 2006-2012 <a href="http://mck.me">Stephen M. McKamey</a>
 	</footer>
 	<script src="/ga.js" type="text/javascript" defer></script>
 	<script src="http://platform.twitter.com/widgets.js" type="text/javascript" defer="defer"></script>
 		var units = ~countdown.ALL,
 			chx = byId('countdown-units').getElementsByTagName('input'),
 			empty = byId('empty-label').value || '',
-			max = +(byId('max-units').value);
+			max = +(byId('max-units').value),
+			digits = +(byId('frac-digits').value);
 
 		for (var i=0, count=chx.length; i<count; i++) {
 			if (chx[i].checked) {
 			fff = +(byId('milliseconds').value);
 
 		var start = new Date(yyyy, MM, dd, HH, mm, ss, fff),
-			ts = countdown(start, null, units);
+			ts = countdown(start, null, units, max);
 
 		var counter = byId('counter'),
 			timespan = byId('timespan'),
-			msg = ts.toHTML('strong', max) || empty;
+			msg = ts.toHTML('strong', digits) || empty;
 
 		if (start.getTime() === 1357027199999) {
 			msg = (ts.value > 0) ?
 
 	<footer>
 		<div style="float:left"><a href="http://twitter.com/share" class="twitter-share-button" data-url="http://countdownjs.org" data-text="Countdown.js: A simple JavaScript API for producing an accurate, intuitive description of the timespan between dates"></a></div>
-		Copyright &copy; 2006-2011 <a href="http://mck.me">Stephen M. McKamey</a>
+		Copyright &copy; 2006-2012 <a href="http://mck.me">Stephen M. McKamey</a>
 	</footer>
 	<script src="/ga.js" type="text/javascript" defer></script>
 	<script src="http://platform.twitter.com/widgets.js" type="text/javascript" defer="defer"></script>
 
 <p>A simple but flexible API is the goal of <em>Countdown.js</em>. There is one global function with a set of static constants:</p>
 
-<pre><code>countdown(start|callback, end|callback, units);</code></pre>
+<pre><code>var timespan = countdown(start|callback, end|callback, units, max);</code></pre>
 
-<p>The parameters are a starting Date, ending Date and an optional set of units. If units is left off, it defaults to <code>countdown.DEFAULTS</code>.</p>
+<p>The parameters are a starting Date, ending Date, an optional set of units, and an optional maximum number of units. If units is left off, it defaults to <code>countdown.DEFAULTS</code>.</p>
 
 <pre><code>countdown.ALL =
 	countdown.MILLENNIA |
 
 <p>This allows a very minimal call to accept the defaults and get the time since/until a single date. For example:</p>
 
-<pre><code>countdown( new Date(2000, 0, 1) );</code></pre>
+<pre><code>countdown( new Date(2000, 0, 1) ).toString();</code></pre>
 
-<p>This will toString() something like:</p>
+<p>This will produce a human readable description like:</p>
 
 <pre><code>11 years, 8 months, 4 days, 10 hours, 12 minutes, and 43 seconds</code></pre>
 
-<h3>Timespan result</h3>
-
-<p>The return value is a Timespan object which always contains the following fields:</p>
-
-<ul>
-	<li><code>Date start</code>: the starting date object used for the calculation</li>
-	<li><code>Date end</code>: the ending date object used for the calculation</li>
-	<li><code>Number units</code>: the units specified</li>
-	<li><code>Number value</code>: total milliseconds difference (i.e., end - start) if end &lt; start this will be negative</li>
-</ul>
-
-<p>Typically the <code>end</code> occurs after <code>start</code> but the arguments were reversed, the only difference is <code>Timespan.value</code> will be negative. The sign of <code>value</code> can be used to determine if the event occurs in the future or in the past. </p>
-
-<p>The following time unit fields are only present if their corresponding units were requested:</p>
-
-<ul>
-	<li><code>Number millennia</code></li>
-	<li><code>Number centuries</code></li>
-	<li><code>Number decades</code></li>
-	<li><code>Number years</code></li>
-	<li><code>Number months</code></li>
-	<li><code>Number days</code></li>
-	<li><code>Number hours</code></li>
-	<li><code>Number minutes</code></li>
-	<li><code>Number seconds</code></li>
-	<li><code>Number milliseconds</code></li>
-</ul>
-
-<p>Finally, Timespan has a few formatting methods:</p>
-
-<ul>
-
-<li><code>toString()</code>: formats the Timespan object as an English sentence. e.g.,
-
-<pre><code>toString() => "5 years, 1 month, 19 days, 12 hours, and 17 minutes"</code></pre></li>
-	
-<li><code>toString(max)</code>: formats the Timespan object as an English sentence, but only returns <code>max</code> most significant units. e.g., using the same input:
-
-<pre><code>toString(2) => "5 years, and 1 month"</code></pre></li>
-
-<li>Negative values of <code>max</code> indicates the number of units to leave off. e.g., using the same input:
-
-<pre><code>toString(-2) => "5 years, 1 month, and 19 days"</code></pre></li>
-
-<li><code>toHTML(tagName)</code>: formats the Timespan object as an English sentence, with the specified HTML tag wrapped around each unit. If no tag name is provided, "<code>span</code>" is used.
-
-<pre><code>ts.toHTML("em") => "&lt;em&gt;5 years&lt;/em&gt;, &lt;em&gt;1 month&lt;/em&gt;, &lt;em&gt;19 days&lt;/em&gt;, &lt;em&gt;12 hours&lt;/em&gt;, and &lt;em&gt;17 minutes&lt;/em&gt;"</code></pre></li>
-
-<li><code>toHTML(tagName, max)</code>: the optional <code>max</code> parameter similarly restricts the total number of units returned. e.g., using the same input:
-
-<pre><code>ts.toHTML("em", 3) => "&lt;em&gt;5 years&lt;/em&gt;, &lt;em&gt;1 month&lt;/em&gt;, and &lt;em&gt;19 days&lt;/em&gt;"
-ts.toHTML("em", -1) => "&lt;em&gt;5 years&lt;/em&gt;, &lt;em&gt;1 month&lt;/em&gt;, &lt;em&gt;19 days&lt;/em&gt;, and &lt;em&gt;12 hours&lt;/em&gt;"</code></pre></li>
-
-</ul>
-
-<p>If <code>start</code> and <code>end</code> are exactly the same (for the requested granularity of units), or <code>max</code> is zero, then <code>toString()</code> and <code>toHTML()</code> will return an empty string.</p>
-
 <h3>The <code>start</code> / <code>end</code> arguments</h3>
 
 <p>The parameters <code>start</code> and <code>end</code> can be one of several values:</p>
 
 <h3>The <code>units</code> argument</h3>
 
-<p>The static units constants can be combined using standard bitwise operators. For example, to explicitly include "months and years" use bitwise-OR:</p>
+<p>The static units constants can be combined using <a href="https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/Bitwise_Operators">standard bitwise operators</a>. For example, to explicitly include "months or days" use bitwise-OR:</p>
 
 <pre><code>countdown.MONTHS | countdown.DAYS</code></pre>
 
 
 <pre><code>~countdown.WEEKS &amp; ~countdown.MILLISECONDS</code></pre>
 
-<p>Equivalently, to specify everything but "not weeks or milliseconds" wrap bitwise-NOT around bitwise-OR:</p>
+<p><a href="http://en.wikipedia.org/wiki/De_Morgan's_laws">Equivalently</a>, to specify everything but "not weeks or milliseconds" wrap bitwise-NOT around bitwise-OR:</p>
 
 <pre><code>~(countdown.WEEKS | countdown.MILLISECONDS)</code></pre>
 
+<h3>The <code>max</code> argument</h3>
+
+<div id="v2.3.0-max" class="breaking-change">
+<h4>Breaking change in v2.3.0!</h4>
+<p>The <code>max</code> argument used to be specified in <code>timespan.toString(&hellip;)</code> and <code>timespan.toHTML(&hellip;)</code>. v2.3.0 moves it to <code>countdown(&hellip;)</code>, which improves efficiency as well as enabling fractional units (<a href="#v2.3.0-digits">see below</a>).</p>
+</div>
+
+The final optional argument specifies a maximum number of unit labels to display. This allows specifying which units are interesting but only displaying the `max` most significant units.
+
+<pre><code>countdown(start, end, units).toString() => "5 years, 1 month, 19 days, 12 hours, and 17 minutes"</code></pre>
+
+<p>Specifying <code>max</code> as <code>2</code> ensures that only the two most significant units are displayed <strong>(note the rounding of months)</strong>:</p>
+
+<pre><code>countdown(start, end, units, 2).toString() => "5 years, and 2 months"</code></pre>
+
+<p>Negative or zero values of <code>max</code> are ignored.</code>
+
+<h3>Timespan result</h3>
+
+<p>The return value is a Timespan object which always contains the following fields:</p>
+
+<ul>
+	<li><code>Date start</code>: the starting date object used for the calculation</li>
+	<li><code>Date end</code>: the ending date object used for the calculation</li>
+	<li><code>Number units</code>: the units specified</li>
+	<li><code>Number value</code>: total milliseconds difference (i.e., <code>end</code> - <code>start</code>). If <code>end &lt; start</code> then <code>value</code> will be negative.</li>
+</ul>
+
+<p>Typically the <code>end</code> occurs after <code>start</code>, but if the arguments were reversed, the only difference is <code>Timespan.value</code> will be negative. The sign of <code>value</code> can be used to determine if the event occurs in the future or in the past. </p>
+
+<p>The following time unit fields are only present if their corresponding units were requested:</p>
+
+<ul>
+	<li><code>Number millennia</code></li>
+	<li><code>Number centuries</code></li>
+	<li><code>Number decades</code></li>
+	<li><code>Number years</code></li>
+	<li><code>Number months</code></li>
+	<li><code>Number days</code></li>
+	<li><code>Number hours</code></li>
+	<li><code>Number minutes</code></li>
+	<li><code>Number seconds</code></li>
+	<li><code>Number milliseconds</code></li>
+</ul>
+
+<p>Finally, Timespan has two formatting methods each with some optional parameters:</p>
+
+<ul>
+
+<li><code>String toString(digits)</code>: formats the Timespan object as an English sentence. The optional <code>digits</code> argument allows fractional values on the smallest unit. e.g., using the same input:
+
+<pre><code>ts.toString() => "5 years, and 2 months"
+ts.toString(2) => "5 years, and 1.65 months"</code></pre></li>
+
+<li><code>String toHTML(tagName, digits)</code>: formats the Timespan object as an English sentence, with the specified HTML tag wrapped around each unit. If no tag name is provided, "<code>span</code>" is used. Again, the optional <code>digits</code> argument allows fractional values on the smallest unit. e.g., using the same input:
+
+<pre><code>ts.toHTML("em") => "&lt;em&gt;5 years&lt;/em&gt;, &lt;em&gt;1 month&lt;/em&gt;, &lt;em&gt;19 days&lt;/em&gt;, &lt;em&gt;12 hours&lt;/em&gt;, and &lt;em&gt;17 minutes&lt;/em&gt;"
+ts.toHTML("em", 3) => "&lt;em&gt;5 years&lt;/em&gt;, &lt;em&gt;1 month&lt;/em&gt;, &lt;em&gt;19 days&lt;/em&gt;, &lt;em&gt;12 hours&lt;/em&gt;, and &lt;em&gt;17.193 minutes&lt;/em&gt;"</code></pre></li>
+
+</ul>
+
+<p>Digits must be between <code>0</code> and <code>20</code>, inclusive. Negative values of <code>digits</code> are ignored.</p>
+
+<p>If <code>start</code> and <code>end</code> are exactly the same or the difference is below the requested granularity of units, then <code>toString()</code> and <code>toHTML()</code> will simply return an empty string.</p>
+
+<div id="v2.3.0-digits" class="breaking-change">
+<h4>Breaking change in v2.3.0!</h4>
+<p>Previously, the <code>max</code> number of unit labels used to be defined when formatting. Now it is specified with the units themselves (<a href="#v2.3.0-max">see above</a>).</p>
+
+<h4>Rounding</h4>
+<p>With the calculations of fractional units in v2.3.0, the smallest displayed unit now properly rounds. Previously, the equivalent of <code>"1.99 years"</code> would be truncated to <code>"1 year"</code>, as of v2.3.0 it will display as <code>"2 years"</code>.</p>
+<p>Typically, this is the intended interpretation but there are a few circumstances where people expect the truncated behavior. For example, people often talk about their age as the lowest possible interpretation. e.g., they claim "39-years-old" right up until the morning of their 40th birthday (some people do even for years after!). In these cases, after calling <code>countdown(&hellip;)</code>, you might want to set <code>ts.years = Math.floor(ts.years)</code> before calling <code>ts.toString(0)</code>. The vain might want you to set <code>ts.years = Math.min(ts.years, 39)</code>!</p>
+</div>
+
 <h2>License</h2>
 
 <p>Distributed under the terms of <a href="https://bitbucket.org/mckamey/countdown.js/raw/tip/LICENSE.txt">The MIT license</a>.</p>
 
 <footer>
 	<div style="float:left"><a href="http://twitter.com/share" class="twitter-share-button" data-url="http://countdownjs.org" data-text="Countdown.js: A simple JavaScript API for producing an accurate, intuitive description of the timespan between dates"></a></div>
-	Copyright &copy; 2006-2011 <a href="http://mck.me">Stephen M. McKamey</a>
+	Copyright &copy; 2006-2012 <a href="http://mck.me">Stephen M. McKamey</a>
 </footer>
 <script src="/ga.js" type="text/javascript" defer></script>
 <script src="http://platform.twitter.com/widgets.js" type="text/javascript" defer="defer"></script>

test/formatTests.js

 
 test('millennium, week; 1 max', function() {
 
-	var input = countdown(0, 10 * 100 * 365.25 * 24 * 60 * 60 * 1000, countdown.ALL);
+	var input = countdown(0, 10 * 100 * 365.25 * 24 * 60 * 60 * 1000, countdown.ALL, 1);
 
 	var expected = '1 millennium';
 
-	var actual = input.toString(1);
+	var actual = input.toString();
 
 	same(actual, expected, '');
 });
 		(60 * 1000) + // min
 		1000 + // sec
 		1, // ms
-		countdown.ALL);
+		countdown.ALL,
+		3);
 
 	var expected = '1 millennium, 1 century, and 1 year';
 
-	var actual = input.toString(3);
+	var actual = input.toString();
 
 	same(actual, expected, '');
 });
 		(60 * 1000) + // min
 		1000 + // sec
 		1, // ms
-		countdown.ALL);
+		countdown.ALL,
+		0);
 
 	var expected = '1 millennium, 1 century, 1 year, 1 month, 1 week, 1 day, 1 hour, 1 minute, 1 second, and 1 millisecond';
 
-	var actual = input.toString(0);
+	var actual = input.toString();
 
 	same(actual, expected, '');
 });
 		(60 * 1000) + // min
 		1000 + // sec
 		1, // ms
-		countdown.ALL);
+		countdown.ALL,
+		-2);
 
 	var expected = '1 millennium, 1 century, 1 year, 1 month, 1 week, 1 day, 1 hour, 1 minute, 1 second, and 1 millisecond';
 
-	var actual = input.toString(-2);
+	var actual = input.toString();
+
+	same(actual, expected, '');
+});
+
+test('Almost 2 minutes, full 3 digits', function() {
+
+	var input = countdown(new Date(915220800000), new Date(915220919999), countdown.DEFAULTS);
+
+	var expected = "1 minute, and 59.999 seconds";
+
+	var actual = input.toString(3);
+
+	same(actual, expected, '');
+});
+
+test('Almost 2 minutes, rounded 2 digits', function() {
+
+	var input = countdown(new Date(915220800000), new Date(915220919999), countdown.DEFAULTS);
+
+	var expected = "2 minutes";
+
+	var actual = input.toString(2);
 
 	same(actual, expected, '');
 });
 	font-family: 'LeagueGothicRegular';
 	src: url('fonts/League_Gothic-webfont.eot');							/* IE9 Compat Modes */
 	src: url('fonts/League_Gothic-webfont.eot?iefix') format('eot'),		/* IE6-IE8 */
-	     url('fonts/League_Gothic-webfont.woff') format('woff'),			/* Modern Browsers */
-	     url('fonts/League_Gothic-webfont.ttf')  format('truetype'),		/* Safari, Android, iOS */
-	     url('fonts/League_Gothic-webfont.svg#svgFontName') format('svg');	/* Legacy iOS */
+		 url('fonts/League_Gothic-webfont.woff') format('woff'),			/* Modern Browsers */
+		 url('fonts/League_Gothic-webfont.ttf')	 format('truetype'),		/* Safari, Android, iOS */
+		 url('fonts/League_Gothic-webfont.svg#svgFontName') format('svg');	/* Legacy iOS */
 	font-weight: normal;
 	font-style: normal;
 }
 	text-align: right;
 }
 
+.breaking-change {
+	border: 1px solid #990000;
+	border-radius: 3px;
+	-mox-border-radius: 3px;
+	-webkit-border-radius: 3px;
+	font-style: italic;
+	padding: 0 1em;
+	margin: 1em 0;
+}
+
+.breaking-change:target {
+	-webkit-animation: target-fade 5s 1;
+	-moz-animation: target-fade 5s 1;
+	-o-animation: target-fade 5s 1;
+	animation: target-fade 5s 1;
+}
+
+@-webkit-keyframes target-fade {
+	0% { background-color: #FFFF66; }
+	100% { background-color: rgba(0,0,0,0); }
+}
+@-moz-keyframes target-fade {
+	0% { background-color: #FFFF66; }
+	100% { background-color: rgba(0,0,0,0); }
+}
+@-o-keyframes target-fade {
+	0% { background-color: #FFFF66; }
+	100% { background-color: rgba(0,0,0,0); }
+}
+@keyframes target-fade {
+	0% { background-color: #FFFF66; }
+	100% { background-color: rgba(0,0,0,0); }
+}
+
 #counter {
 	border: 0;
 	font-size: 250%;

test/timespanTests.js

 	same(actual, expected, ''+start+' => '+end);
 });
 
+test('Almost 2 minutes', function() {
+
+	var start = new Date(915220800000);
+	var end = new Date(915220919999);
+
+	var expected = countdown.clone({
+		start: new Date(915220800000),
+		end: new Date(915220919999),
+		units: countdown.DEFAULTS,
+		value: 119999,
+		years: 0,
+		months: 0,
+		days: 0,
+		hours: 0,
+		minutes: 1,
+		seconds: 59.999
+	});
+
+	var actual = countdown(start, end, countdown.DEFAULTS);
+
+	same(actual, expected, ''+start+' => '+end);
+});
+
 }catch(ex){alert(ex);}
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.