Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Add cached format functions for 3x faster formatting. #317

Merged
merged 3 commits into from

5 participants

Tim Wood Rocky Meza Adam Brunner Dmitriy Kiriyenko Isaac Cambron
Tim Wood
Owner

Not ready to pull in, opening discussion.

TODO: inline L LL LLL LLLL LT tokens.

Some speed tests:

http://jsperf.com/date-formatting/8
http://jsperf.com/momentjs-cached-format-functions

All make test-moment tests are passing now, need to add support for L LL LLL LLLL LT for all tests to pass.

File size difference
-108b minified
+75b gzipped

Though there are probably some byte squeezing techniques we could add.

Tim Wood timrwood Cached format functions like xaprb
Still need to inline L LL LLL LLLL LT functions…
6529d56
Tim Wood
Owner

Also, we can probably do something similar with the parser.

Rocky Meza

This could definitely use some real commenting. I know what this is doing, but only because I read about the different approaches to date formatting that you investigated. This is a huge booby trap for somebody who may wish to extend moment's formatting.

Additionally, perhaps this could be an opportunity to make the formatter extensible on the run. Because formatters are now just strings in a hash, why not allow users to add their own formats?

Owner

Yeah, if everyone thinks this should be merged in, I'll definitely add more inline comments.

Adding user created tokens sounds like a great idea. We'll need to recompile the regex whenever tokens are added and invalidate any previously generated functions as well, but its definitely possible now!

We'll also probably need to sort the tokens by length (longest to shortest) so if someone adds a token like Y = 'blah' it doesn't format "YYYY" to "blahblahblahblah".

What are your thoughts about replacing existing tokens? Should people be able to do moment.addToken("MM" : "t.year()") and have it replace the existing MM token? On one hand I can see how it would be useful if someone wanted to completely change the token system to strftime or php or true LDML tokens, but on the other hand, it could cause some confusion if a third party plugin changed the tokens and you weren't aware of it.

I personally feel the original moment format tokens should never change, but if people want a completely different token language they should use something like the moment-strftime wrapper and not the core formatting tokens.

Collaborator

My take on that is to value flexibility over protection. If people want to break stuff in all sorts of horrible ways, I think that's up to them. They won't stumble on this by accident.

Owner

Yeah, I guess so.

I don't think we need to actively pursue this end though. However, if we keep it in mind when we are changing the formatter, we can gradually build toward an extensible one. I don't think there is any demand for this and I don't think there is any reason to build this feature yet.

Owner

Cool, it does add a bit of work when we need to expose the tokens. Every time a token is changed or added, we need to recompile the regex and destroy any previously created functions.

Maybe we can push it out to the 2.0 release.

Adam Brunner adambrunner commented on the diff
moment.js
@@ -23,7 +23,9 @@
aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
// format tokens
- formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|dddd?|do?|w[o|w]?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|zz?|ZZ?|LT|LL?L?L?)/g,
+ formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|dddd?|do?|w[o|w]?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|zz?|ZZ?)/g,
+ localFormattingTokens = /(LT|LL?L?L?)/g,

You can save 1 byte by using /(LT|L{1,4})/g

I also believe that /L{1,4}/ works faster then /LL?L?L?/.

And same pattern used in the line above. I'd say, abused =)

Tim Wood Owner

Performance looks about the same between /(LT|L{1,4})/g and /(LT|LL?L?L?)/g. The saved byte is good though!

http://jsperf.com/regex-optional-vs-length

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Adam Brunner adambrunner commented on the diff
moment.js
@@ -23,7 +23,9 @@
aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
// format tokens
- formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|dddd?|do?|w[o|w]?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|zz?|ZZ?|LT|LL?L?L?)/g,
+ formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|dddd?|do?|w[o|w]?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|zz?|ZZ?)/g,

I've played a bit and this expression can be shortened to this:
formattingTokens = /(\[[^\[]*\])|(\\)?([MD]{1,4}|d|dddd?|ww?|[MDdw]o|DDDo|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|zz?|ZZ?)/g,

It can save 10 bytes.

Explanation of the changes:
MM?M?M?|DD?D?D? became [MD]{1,4}
Mo|Do|do|wo became [MDdw]o
dddd?|do? became d|dddd
w[o|w]? became ww?

Can't understand the two latest. Looks like you're missing o.

It isn't. The "do" and "wo" is covered by the second expression "[MDdw]o".

Oh, I see now.

Tim Wood Owner

Nice work! [MDdw]o will need to go before [MD]{1,4} and d|dddd?|ww? otherwise it will match just the M of Mo.

Also, we are adding a dd token in another pull request so [MD]{1,4} can become [MDd]{1,4} and drop another 5 bytes.

Tim Wood Owner

Oh wait, [MD]{1,4} would also match MMDD and not split it into two different formatting tokens. It would need to be M{1,4}|D{1,4}

You're right, my fault! So the final pattern should be /(\[[^\[]*\])|(\\)?([MDdw]o|DDDo|d{1,4}|D{1,4}|M{1,4}|ww?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|S{1,3}|zz?|ZZ?)/g, if you introduce the dd format too. And if I didn't miss anything again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Tim Wood timrwood referenced this pull request
Closed

1.7.0 Changelog #288

Tim Wood
Owner

For exposing formatting tokens, I was thinking of using this syntax.

moment.formatToken('MM', 'p(t.month()+1,2)'); // set a format token
moment.formatToken('MM'); // 'p(t.month()+1,2)' (get a format token)

Alternatively, we could use token, but that might be insufficiently named as parsing tokens are not affected. I don't plan on exposing parsing tokens (and it would be difficult with the incremental parser) but we may need to in the future...

moment.token('MM', 'p(t.month()+1,2)'); // set a format token
moment.token('MM'); // 'p(t.month()+1,2)' (get a format token)
Rocky Meza

I think that the cached formatters should go out in 1.7, but if we ever do do extensible formatting that should not. The 3x boost for 1.7 seems to be ready already

Tim Wood timrwood merged commit 4996b6b into from
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on May 25, 2012
  1. Tim Wood

    Cached format functions like xaprb

    timrwood authored
    Still need to inline L LL LLL LLLL LT functions…
  2. Tim Wood
  3. Tim Wood

    Trimming some bytes…

    timrwood authored
This page is out of date. Refresh to see the latest.
Showing with 96 additions and 126 deletions.
  1. +1 −1  lang/da.js
  2. +95 −125 moment.js
2  lang/da.js
View
@@ -38,7 +38,7 @@
yy : "%d år"
},
ordinal : function (number) {
- return '.';
+ return '.';
}
};
220 moment.js
View
@@ -23,7 +23,9 @@
aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
// format tokens
- formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|dddd?|do?|w[o|w]?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|zz?|ZZ?|LT|LL?L?L?)/g,
+ formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|dddd?|do?|w[o|w]?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|zz?|ZZ?)/g,

I've played a bit and this expression can be shortened to this:
formattingTokens = /(\[[^\[]*\])|(\\)?([MD]{1,4}|d|dddd?|ww?|[MDdw]o|DDDo|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|zz?|ZZ?)/g,

It can save 10 bytes.

Explanation of the changes:
MM?M?M?|DD?D?D? became [MD]{1,4}
Mo|Do|do|wo became [MDdw]o
dddd?|do? became d|dddd
w[o|w]? became ww?

Can't understand the two latest. Looks like you're missing o.

It isn't. The "do" and "wo" is covered by the second expression "[MDdw]o".

Oh, I see now.

Tim Wood Owner

Nice work! [MDdw]o will need to go before [MD]{1,4} and d|dddd?|ww? otherwise it will match just the M of Mo.

Also, we are adding a dd token in another pull request so [MD]{1,4} can become [MDd]{1,4} and drop another 5 bytes.

Tim Wood Owner

Oh wait, [MD]{1,4} would also match MMDD and not split it into two different formatting tokens. It would need to be M{1,4}|D{1,4}

You're right, my fault! So the final pattern should be /(\[[^\[]*\])|(\\)?([MDdw]o|DDDo|d{1,4}|D{1,4}|M{1,4}|ww?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|S{1,3}|zz?|ZZ?)/g, if you introduce the dd format too. And if I didn't miss anything again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ localFormattingTokens = /(LT|LL?L?L?)/g,

You can save 1 byte by using /(LT|L{1,4})/g

I also believe that /L{1,4}/ works faster then /LL?L?L?/.

And same pattern used in the line above. I'd say, abused =)

Tim Wood Owner

Performance looks about the same between /(LT|L{1,4})/g and /(LT|LL?L?L?)/g. The saved byte is good though!

http://jsperf.com/regex-optional-vs-length

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ formattingRemoveEscapes = /(^\[)|(\\)|\]$/g,
// parsing tokens
parseMultipleFormatChunker = /([0-9a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)/gi,
@@ -63,7 +65,54 @@
'Days' : 864e5,
'Months' : 2592e6,
'Years' : 31536e6
- };
+ },
+
+ // format function strings
+ formatFunctions = {},
+ formatFunctionStrings = {
+ // a = placeholder
+ // b = placeholder
+ // t = the current moment being formatted
+ // v = getValueAtKey function
+ // o = ordinal
+ // p = pad
+ // m = meridiem
+ M : '(a=t.month()+1)',
+ MMM : 'v("monthsShort",t.month())',
+ MMMM : 'v("months",t.month())',
+ D : '(a=t.date())',
+ DDD : '(a=new Date(t.year(),t.month(),t.date()),b=new Date(t.year(),0,1),a=~~(((a-b)/864e5)+1.5))',
+ d : '(a=t.day())',
+ ddd : 'v("weekdaysShort",t.day())',
+ dddd : 'v("weekdays",t.day())',
+ w : '(a=new Date(t.year(),t.month(),t.date()-t.day()+5),b=new Date(a.getFullYear(),0,4),a=~~((a-b)/864e5/7+1.5))',
+ YY : 'p(t.year()%100,2)',
+ YYYY : 't.year()',
+ a : 'm?m(t.hours(),t.minutes(),!1):t.hours()>11?"pm":"am"',
+ A : 'm?m(t.hours(),t.minutes(),!0):t.hours()>11?"PM":"AM"',
+ H : 't.hours()',
+ h : 't.hours()%12||12',
+ m : 't.minutes()',
+ s : 't.seconds()',
+ S : '~~(t.milliseconds()/100)',
+ SS : 'p(~~(t.milliseconds()/10),2)',
+ SSS : 'p(t.milliseconds(),3)',
+ Z : '((a=-t.zone())<0?((a=-a),"-"):"+")+p(~~(a/60),2)+":"+p(~~a%60,2)',
+ ZZ : '((a=-t.zone())<0?((a=-a),"-"):"+")+p(~~(10*a/6),4)'
+ },
+
+ ordinalizeTokens = 'DDD w M D d'.split(' '),
+ paddedTokens = 'M D d H h m s w'.split(' ');
+
+ while (ordinalizeTokens.length) {
+ i = ordinalizeTokens.pop();
+ formatFunctionStrings[i + 'o'] = formatFunctionStrings[i] + '+o(a)';
+ }
+ while (paddedTokens.length) {
+ i = paddedTokens.pop();
+ formatFunctionStrings[i + i] = 'p(' + formatFunctionStrings[i] + ',2)';
+ }
+ formatFunctionStrings.DDDD = 'p(' + formatFunctionStrings.DDD + ',3)';
// Moment prototype object
function Moment(date, isUTC) {
@@ -174,132 +223,53 @@
function dateFromArray(input) {
return new Date(input[0], input[1] || 0, input[2] || 1, input[3] || 0, input[4] || 0, input[5] || 0, input[6] || 0);
}
+
+ function replaceFormatTokens(token) {
+ return formatFunctionStrings[token] ?
+ ("'+(" + formatFunctionStrings[token] + ")+'") :
+ token.replace(formattingRemoveEscapes, "").replace(/\\?'/g, "\\'");
+ }
+
+ function replaceLongDateFormatTokens(input) {
+ return moment.longDateFormat[input] || input;
+ }
+
+ function makeFormatFunction(format) {
+ var output = "var a,b;return '" +
+ format.replace(formattingTokens, replaceFormatTokens) + "';",
+ Fn = Function; // get around jshint
+ // a = placeholder
+ // b = placeholder
+ // t = the current moment being formatted
+ // v = getValueAtKey function
+ // o = ordinal
+ // p = pad
+ // m = meridiem
+ return new Fn('t', 'v', 'o', 'p', 'm', output);
+ }
+
+ function makeOrGetFormatFunction(format) {
+ if (!formatFunctions[format]) {
+ formatFunctions[format] = makeFormatFunction(format);
+ }
+ return formatFunctions[format];
+ }
// format date using native date object
- function formatMoment(m, inputString) {
- var currentMonth = m.month(),
- currentDate = m.date(),
- currentYear = m.year(),
- currentDay = m.day(),
- currentHours = m.hours(),
- currentMinutes = m.minutes(),
- currentSeconds = m.seconds(),
- currentMilliseconds = m.milliseconds(),
- currentZone = -m.zone(),
- ordinal = moment.ordinal,
- meridiem = moment.meridiem;
- // check if the character is a format
- // return formatted string or non string.
- //
- // uses switch/case instead of an object of named functions (like http://phpjs.org/functions/date:380)
- // for minification and performance
- // see http://jsperf.com/object-of-functions-vs-switch for performance comparison
- function replaceFunction(input) {
- // create a couple variables to be used later inside one of the cases.
- var a, b;
- switch (input) {
- // MONTH
- case 'M' :
- return currentMonth + 1;
- case 'Mo' :
- return (currentMonth + 1) + ordinal(currentMonth + 1);
- case 'MM' :
- return leftZeroFill(currentMonth + 1, 2);
- case 'MMM' :
- return moment.monthsShort[currentMonth];
- case 'MMMM' :
- return moment.months[currentMonth];
- // DAY OF MONTH
- case 'D' :
- return currentDate;
- case 'Do' :
- return currentDate + ordinal(currentDate);
- case 'DD' :
- return leftZeroFill(currentDate, 2);
- // DAY OF YEAR
- case 'DDD' :
- a = new Date(currentYear, currentMonth, currentDate);
- b = new Date(currentYear, 0, 1);
- return ~~ (((a - b) / 864e5) + 1.5);
- case 'DDDo' :
- a = replaceFunction('DDD');
- return a + ordinal(a);
- case 'DDDD' :
- return leftZeroFill(replaceFunction('DDD'), 3);
- // WEEKDAY
- case 'd' :
- return currentDay;
- case 'do' :
- return currentDay + ordinal(currentDay);
- case 'ddd' :
- return moment.weekdaysShort[currentDay];
- case 'dddd' :
- return moment.weekdays[currentDay];
- // WEEK OF YEAR
- case 'w' :
- a = new Date(currentYear, currentMonth, currentDate - currentDay + 5);
- b = new Date(a.getFullYear(), 0, 4);
- return ~~ ((a - b) / 864e5 / 7 + 1.5);
- case 'wo' :
- a = replaceFunction('w');
- return a + ordinal(a);
- case 'ww' :
- return leftZeroFill(replaceFunction('w'), 2);
- // YEAR
- case 'YY' :
- return leftZeroFill(currentYear % 100, 2);
- case 'YYYY' :
- return currentYear;
- // AM / PM
- case 'a' :
- return meridiem ? meridiem(currentHours, currentMinutes, false) : (currentHours > 11 ? 'pm' : 'am');
- case 'A' :
- return meridiem ? meridiem(currentHours, currentMinutes, true) : (currentHours > 11 ? 'PM' : 'AM');
- // 24 HOUR
- case 'H' :
- return currentHours;
- case 'HH' :
- return leftZeroFill(currentHours, 2);
- // 12 HOUR
- case 'h' :
- return currentHours % 12 || 12;
- case 'hh' :
- return leftZeroFill(currentHours % 12 || 12, 2);
- // MINUTE
- case 'm' :
- return currentMinutes;
- case 'mm' :
- return leftZeroFill(currentMinutes, 2);
- // SECOND
- case 's' :
- return currentSeconds;
- case 'ss' :
- return leftZeroFill(currentSeconds, 2);
- // MILLISECONDS
- case 'S' :
- return ~~ (currentMilliseconds / 100);
- case 'SS' :
- return leftZeroFill(~~(currentMilliseconds / 10), 2);
- case 'SSS' :
- return leftZeroFill(currentMilliseconds, 3);
- // TIMEZONE
- case 'Z' :
- return (currentZone < 0 ? '-' : '+') + leftZeroFill(~~(Math.abs(currentZone) / 60), 2) + ':' + leftZeroFill(~~(Math.abs(currentZone) % 60), 2);
- case 'ZZ' :
- return (currentZone < 0 ? '-' : '+') + leftZeroFill(~~(10 * Math.abs(currentZone) / 6), 4);
- // LONG DATES
- case 'L' :
- case 'LL' :
- case 'LLL' :
- case 'LLLL' :
- case 'LT' :
- return formatMoment(m, moment.longDateFormat[input]);
- // DEFAULT
- default :
- return input.replace(/(^\[)|(\\)|\]$/g, "");
- }
+ function formatMoment(m, format) {
+ function getValueFromArray(key, index) {
+ return moment[key].call ? moment[key](m, format) : moment[key][index];
}
- return inputString.replace(formattingTokens, replaceFunction);
+
+ while (localFormattingTokens.test(format)) {
+ format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
+ }
+
+ if (!formatFunctions[format]) {
+ formatFunctions[format] = makeFormatFunction(format);
+ }
+
+ return formatFunctions[format](m, getValueFromArray, moment.ordinal, leftZeroFill, moment.meridiem);
}
// get the regex to find the next token
Something went wrong with that request. Please try again.