diff --git a/strftimeV2.js b/strftimeV2.js new file mode 100644 index 0000000..928a810 --- /dev/null +++ b/strftimeV2.js @@ -0,0 +1,497 @@ +// +// strftime +// github.com/samsonjs/strftime +// @_sjs +// +// Copyright 2010 - 2013 Sami Samhuri +// +// MIT License +// http://sjs.mit-license.org +// + +;(function() { + "use strict"; + + //// Where to export the API + var namespace, + + DefaultLocale = { + days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + shortDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], + shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + AM: 'AM', + PM: 'PM', + am: 'am', + pm: 'pm', + formats: { + D: '%m/%d/%y', + F: '%Y-%m-%d', + R: '%H:%M', + T: '%H:%M:%S', + r: '%I:%M:%S %p', + v: '%e-%b-%Y' + } + }, + + defaultStrftime = new Strftime(DefaultLocale, 0, false); + + // CommonJS / Node module + if (typeof module !== 'undefined') { + namespace = module.exports = defaultStrftime; + } + + // Browsers and other environments + else { + // Get the global object. Works in ES3, ES5, and ES5 strict mode. + namespace = (function(){ return this || (1,eval)('this') }()); + } + + namespace.strftime = defaultStrftime; + + function Strftime(locale, customTimezoneOffset, useUtcTimezone) { + var _locale = locale || DefaultLocale, + _customTimezoneOffset = customTimezoneOffset || 0, + _useUtcBasedDate = useUtcTimezone || false, + + // we store unix timestamp value here to not create new Date() each iteration (each millisecond) + // Date.now() is 2 times faster than new Date() + // while millisecond precise is enough here + // this could be very helpful when strftime triggered a lot of times one by one + _cachedDateTimestamp = 0, + _cachedDate; + + function _strftime(format, date) { + var timestamp; + + if(!date) { + var currentTimestamp = Date.now(); + if(currentTimestamp > _cachedDateTimestamp) { + _cachedDateTimestamp = currentTimestamp; + _cachedDate = new Date(_cachedDateTimestamp); + + timestamp = _cachedDateTimestamp; + + if(_useUtcBasedDate) { + // how to avoid duplication of date instantiation for utc here? + // we tied to getTimezoneOffset of the current date + _cachedDate = new Date(_cachedDateTimestamp + getTimestampToUtcOffsetFor(_cachedDate) + _customTimezoneOffset); + } + } + date = _cachedDate; + } else { + timestamp = date.getTime(); + + if(_useUtcBasedDate) { + date = new Date(date.getTime() + getTimestampToUtcOffsetFor(date) + _customTimezoneOffset); + } + } + + return _processFormat(format, date, _locale, timestamp); + } + + function _processFormat(format, date, locale, timestamp) { + var resultString = '', + padding = null, + isInScope = false, + length = format.length; + + for (var i = 0; i < length; i++) { + + var currentCharCode = format.charCodeAt(i); + + if(isInScope === true) { + // '-' + if (currentCharCode === 45) { + padding = ''; + continue; + } + // '_' + else if (currentCharCode === 95) { + padding = ' '; + continue; + } + // '0' + else if (currentCharCode === 48) { + padding = '0'; + continue; + } + + switch (currentCharCode) { + + // Examples for new Date(0) in GMT + + // 'Thursday' + // case 'A': + case 65: + resultString += locale.days[date.getDay()]; + break; + + // 'January' + // case 'B': + case 66: + resultString += locale.months[date.getMonth()]; + break; + + // '19' + // case 'C': + case 67: + resultString += padTill2(Math.floor(date.getFullYear() / 100), padding); + break; + + // '01/01/70' + // case 'D': + case 68: + resultString += _processFormat(locale.formats.D, date, locale, timestamp); + break; + + // '1970-01-01' + // case 'F': + case 70: + resultString += _processFormat(locale.formats.F, date, locale, timestamp); + break; + + // '00' + // case 'H': + case 72: + resultString += padTill2(date.getHours(), padding); + break; + + // '12' + // case 'I': + case 73: + resultString += padTill2(hours12(date.getHours()), padding); + break; + + // '000' + // case 'L': + case 76: + resultString += padTill3(Math.floor(timestamp % 1000)); + break; + + // '00' + // case 'M': + case 77: + resultString += padTill2(date.getMinutes(), padding); + break; + + // 'am' + // case 'P': + case 80: + resultString += date.getHours() < 12 ? locale.am : locale.pm; + break; + + // '00:00' + // case 'R': + case 82: + resultString += _processFormat(locale.formats.R, date, locale, timestamp); + break; + + // '00' + // case 'S': + case 83: + resultString += padTill2(date.getSeconds(), padding); + break; + + // '00:00:00' + // case 'T': + case 84: + resultString += _processFormat(locale.formats.T, date, locale, timestamp); + break; + + // '00' + // case 'U': + case 85: + resultString += padTill2(weekNumber(date, 'sunday'), padding); + break; + + // '00' + // case 'W': + case 87: + resultString += padTill2(weekNumber(date, 'monday'), padding); + break; + + // '1970' + // case 'Y': + case 89: + resultString += date.getFullYear(); + break; + + // 'GMT' + // case 'Z': + case 90: + if (_useUtcBasedDate && _customTimezoneOffset === 0) { + resultString += "GMT"; + } + else { + // fixme optimize + var tzString = date.toString().match(/\((\w+)\)/); + resultString += tzString && tzString[1] || ''; + } + break; + + // 'Thu' + // case 'a': + case 97: + resultString += locale.shortDays[date.getDay()]; + break; + + // 'Jan' + // case 'b': + case 98: + resultString += locale.shortMonths[date.getMonth()]; + break; + + // '01' + // case 'd': + case 100: + resultString += padTill2(date.getDate(), padding); + break; + + // '01' + // case 'e': + case 101: + resultString += date.getDate(); + break; + + // 'Jan' + // case 'h': + case 104: + resultString += locale.shortMonths[date.getMonth()]; + break; + + // '000' + // case 'j': + case 106: + var y = new Date(date.getFullYear(), 0, 1); + var day = Math.ceil((date.getTime() - y.getTime()) / (1000 * 60 * 60 * 24)); + resultString += padTill3(day); + break; + + // ' 0' + // case 'k': + case 107: + resultString += padTill2(date.getHours(), padding == null ? ' ' : padding); + break; + + // '12' + // case 'l': + case 108: + resultString += padTill2(hours12(date.getHours()), padding == null ? ' ' : padding); + break; + + // '01' + // case 'm': + case 109: + resultString += padTill2(date.getMonth() + 1, padding); + break; + + // '\n' + // case 'n': + case 110: + resultString += '\n'; + break; + + // '1st' + // case 'o': + case 111: + resultString += String(date.getDate()) + ordinal(date.getDate()); + break; + + // 'AM' + // case 'p': + case 112: + resultString += date.getHours() < 12 ? locale.AM : locale.PM; + break; + + // '12:00:00 AM' + // case 'r': + case 114: + resultString += _processFormat(locale.formats.r, date, locale, timestamp); + break; + + // '0' + // case 's': + case 115: + resultString += Math.floor(timestamp / 1000); + break; + + // '\t' + // case 't': + case 116: + resultString += '\t'; + break; + + // '4' + // case 'u': + case 117: + var day = date.getDay(); + resultString += day === 0 ? 7 : day; + break; // 1 - 7, Monday is first day of the week + + // '1-Jan-1970' + // case 'v': + case 118: + resultString += _processFormat(locale.formats.v, date, locale, timestamp); + break; + + // '4' + // case 'w': + case 119: + resultString += date.getDay(); + break; // 0 - 6, Sunday is first day of the week + + // '70' + // case 'y': + case 121: + resultString += ('' + date.getFullYear()).slice(2); + break; + + // '+0000' + // case 'z': + case 122: + if (_useUtcBasedDate && _customTimezoneOffset === 0) { + resultString += "+0000"; + } + else { + var off; + if(_customTimezoneOffset !== 0) { + off = _customTimezoneOffset / (60 * 1000); + } + else { + off = -date.getTimezoneOffset(); + } + resultString += (off < 0 ? '-' : '+') + padTill2(Math.floor(Math.abs(off / 60))) + padTill2(Math.abs(off % 60)); + } + break; + + default: + resultString += format[i]; + break; + } + + padding = null; + isInScope = false; + continue; + } + + // '%' + if (currentCharCode === 37) { + isInScope = true; + continue; + } + + resultString += format[i]; + } + + return resultString; + } + + var strftime = _strftime; + + strftime.setLocaleTo = function(locale) { + return new Strftime(locale || _locale, _customTimezoneOffset, _useUtcBasedDate); + }; + + strftime.setTimezoneTo = function(timezone) { + var customTimezoneOffset = _customTimezoneOffset; + var useUtcBasedDate = _useUtcBasedDate; + + var timezoneType = typeof timezone; + if (timezoneType === 'number' || timezoneType === 'string') { + useUtcBasedDate = true; + + // ISO 8601 format timezone string, [-+]HHMM + if (timezoneType === 'string') { + var sign = timezone[0] === '-' ? -1 : 1, + hours = parseInt(timezone.slice(1, 3), 10), + minutes = parseInt(timezone.slice(3, 5), 10); + + customTimezoneOffset = sign * ((60 * hours) + minutes) * 60 * 1000; + // in minutes: 420 + } else if (timezoneType === 'number'){ + customTimezoneOffset = timezone * 60 * 1000; + } + } + + return new Strftime(_locale, customTimezoneOffset, useUtcBasedDate); + }; + + strftime.useUTC = function() { + return new Strftime(_locale, _customTimezoneOffset, true); + }; + + return strftime; + } + + function padTill2(numberToPad, paddingChar) { + if (paddingChar === '' || numberToPad > 9) { + return numberToPad; + } + if (paddingChar == null) { + paddingChar = '0'; + } + return paddingChar + numberToPad; + } + + function padTill3(numberToPad) { + if (numberToPad > 99) { + return numberToPad; + } + if (numberToPad > 9) { + return '0' + numberToPad; + } + return '00' + numberToPad; + } + + function hours12(hour) { + if (hour === 0) { + return 12; + } + else if (hour > 12) { + return hour - 12; + } + return hour; + } + + // firstWeekday: 'sunday' or 'monday', default is 'sunday' + // + // Pilfered & ported from Ruby's strftime implementation. + function weekNumber(date, firstWeekday) { + firstWeekday = firstWeekday || 'sunday'; + + // This works by shifting the weekday back by one day if we + // are treating Monday as the first day of the week. + var weekday = date.getDay(); + if (firstWeekday === 'monday') { + if (weekday === 0) // Sunday + weekday = 6; + else + weekday--; + } + var firstDayOfYear = new Date(date.getFullYear(), 0, 1), + yday = (date - firstDayOfYear) / 86400000, + weekNum = (yday + 7 - weekday) / 7; + + return Math.floor(weekNum); + } + + // Get the ordinal suffix for a number: st, nd, rd, or th + function ordinal(number) { + var i = number % 10; + var ii = number % 100; + + if ((ii >= 11 && ii <= 13) || i === 0 || i >= 4) { + return 'th'; + } + switch (i) { + case 1: return 'st'; + case 2: return 'nd'; + case 3: return 'rd'; + } + } + + function getTimestampToUtcOffsetFor(date) { + return (date.getTimezoneOffset() || 0) * 60000; + } +}()); diff --git a/test/testV2.js b/test/testV2.js new file mode 100644 index 0000000..61d0f6c --- /dev/null +++ b/test/testV2.js @@ -0,0 +1,224 @@ +#!/usr/bin/env node + +// Based on CoffeeScript by andrewschaaf on github +// +// TODO: +// - past and future dates, especially < 1900 and > 2100 +// - locales +// - look for edge cases + +var assert = require('assert') + , libFilename = process.argv[2] || '../strftimeV2.js' + , lib = require(libFilename) + +// Tue, 07 Jun 2011 18:51:45 GMT + , Time = new Date(1307472705067) + +assert.fn = function(value, msg) { + assert.equal('function', typeof value, msg) +} + +assert.format = function(format, expected, expectedUTC, time) { + time = time || Time + function _assertFmt(expected, name, strftime) { + name = name || 'strftime' + var actual = strftime(format, time) + assert.equal(expected, actual, + name + '("' + format + '", ' + time + ') is ' + JSON.stringify(actual) + + ', expected ' + JSON.stringify(expected)) + } + + if (expected) _assertFmt(expected, 'strftime', lib) + _assertFmt(expectedUTC || expected, 'strftime.UseUTC()', lib.useUTC()) +} + +/// check exports +assert.fn(lib.strftime) +ok('Exports') + +/// time zones +if (process.env.TZ == 'America/Vancouver') { + testTimezone('P[DS]T') + assert.format('%C', '01', '01', new Date(100, 0, 1)) + assert.format('%j', '097', '098', new Date(1365390736236)) + ok('Time zones (' + process.env.TZ + ')') +} +else if (process.env.TZ == 'CET') { + testTimezone('CES?T') + assert.format('%C', '01', '00', new Date(100, 0, 1)) + assert.format('%j', '098', '098', new Date(1365390736236)) + ok('Time zones (' + process.env.TZ + ')') +} +else { + console.log('(Current timezone has no tests: ' + (process.env.TZ || 'none') + ')') +} + +/// check all formats in GMT, most coverage +assert.format('%A', 'Tuesday') +assert.format('%a', 'Tue') +assert.format('%B', 'June') +assert.format('%b', 'Jun') +assert.format('%C', '20') +assert.format('%D', '06/07/11') +assert.format('%d', '07') +assert.format('%-d', '7') +assert.format('%_d', ' 7') +assert.format('%0d', '07') +assert.format('%e', '7') +assert.format('%F', '2011-06-07') +assert.format('%H', null, '18') +assert.format('%h', 'Jun') +assert.format('%I', null, '06') +assert.format('%-I', null, '6') +assert.format('%_I', null, ' 6') +assert.format('%0I', null, '06') +assert.format('%j', null, '158') +assert.format('%k', null, '18') +assert.format('%L', '067') +assert.format('%l', null, ' 6') +assert.format('%-l', null, '6') +assert.format('%_l', null, ' 6') +assert.format('%0l', null, '06') +assert.format('%M', null, '51') +assert.format('%m', '06') +assert.format('%n', '\n') +assert.format('%o', '7th') +assert.format('%P', null, 'pm') +assert.format('%p', null, 'PM') +assert.format('%R', null, '18:51') +assert.format('%r', null, '06:51:45 PM') +assert.format('%S', '45') +assert.format('%s', '1307472705') +assert.format('%T', null, '18:51:45') +assert.format('%t', '\t') +assert.format('%U', '23') +assert.format('%U', '24', null, new Date(+Time + 5 * 86400000)) +assert.format('%u', '2') +assert.format('%v', '7-Jun-2011') +assert.format('%W', '23') +assert.format('%W', '23', null, new Date(+Time + 5 * 86400000)) +assert.format('%w', '2') +assert.format('%Y', '2011') +assert.format('%y', '11') +assert.format('%Z', null, 'GMT') +assert.format('%z', null, '+0000') +assert.format('%%', '%') // any other char +//assert.format('%--', '-') +ok('GMT') + + +/// locales + +var it_IT = +{ days: words('domenica lunedi martedi mercoledi giovedi venerdi sabato') + , shortDays: words('dom lun mar mer gio ven sab') + , months: words('gennaio febbraio marzo aprile maggio giugno luglio agosto settembre ottobre novembre dicembre') + , shortMonths: words('gen feb mar apr mag giu lug ago set ott nov dic') + , AM: 'it$AM' + , PM: 'it$PM' + , am: 'it$am' + , pm: 'it$pm' + , formats: { + D: 'it$%m/%d/%y' + , F: 'it$%Y-%m-%d' + , R: 'it$%H:%M' + , r: 'it$%I:%M:%S %p' + , T: 'it$%H:%M:%S' + , v: 'it$%e-%b-%Y' +} +} + +assert.format_it = function(format, expected, expectedUTC) { + function _assertFmt(expected, name, strftime) { + name = name || 'strftime' + var actual = strftime(format, Time) + assert.equal(expected, actual, + name + '("' + format + '", Time) is ' + JSON.stringify(actual) + + ', expected ' + JSON.stringify(expected)) + } + + if (expected) _assertFmt(expected, 'strftime', lib.setLocaleTo(it_IT)) + _assertFmt(expectedUTC || expected, 'strftimeUTC', lib.useUTC().setLocaleTo(it_IT)) +} + +assert.format_it('%A', 'martedi') +assert.format_it('%a', 'mar') +assert.format_it('%B', 'giugno') +assert.format_it('%b', 'giu') +assert.format_it('%D', 'it$06/07/11') +assert.format_it('%F', 'it$2011-06-07') +assert.format_it('%p', null, 'it$PM') +assert.format_it('%P', null, 'it$pm') +assert.format_it('%R', null, 'it$18:51') +assert.format_it('%r', null, 'it$06:51:45 it$PM') +assert.format_it('%T', null, 'it$18:51:45') +assert.format_it('%v', 'it$7-giu-2011') +ok('Localization') + + +/// timezones + +assert.formatTZ = function(format, expected, tz, time) { + time = time || Time; + var actual = lib.setTimezoneTo(tz)(format, time) + assert.equal( + expected, actual, + ('strftime.setTimezoneTo()("' + format + '", ' + time + ') is ' + JSON.stringify(actual) + ', expected ' + JSON.stringify(expected)) + ) +} + +assert.formatTZ('%F %r %z', '2011-06-07 06:51:45 PM +0000', 0) +assert.formatTZ('%F %r %z', '2011-06-07 06:51:45 PM +0000', '+0000') +assert.formatTZ('%F %r %z', '2011-06-07 08:51:45 PM +0200', 120) +assert.formatTZ('%F %r %z', '2011-06-07 08:51:45 PM +0200', '+0200') +assert.formatTZ('%F %r %z', '2011-06-07 11:51:45 AM -0700', -420) +assert.formatTZ('%F %r %z', '2011-06-07 11:51:45 AM -0700', '-0700') +assert.formatTZ('%F %r %z', '2011-06-07 11:21:45 AM -0730', '-0730') +ok('Time zone offset') + + +/// helpers + +function words(s) { return (s || '').split(' '); } + +function ok(s) { console.log('[ \033[32mOK\033[0m ] ' + s) } + +// Pass a regex or string that matches the timezone abbrev, e.g. %Z above. +// Don't pass GMT! Every date includes it and it will fail. +// Be careful if you pass a regex, it has to quack like the default one. +function testTimezone(regex) { + regex = typeof regex === 'string' ? RegExp('\\((' + regex + ')\\)$') : regex + var match = Time.toString().match(regex) + if (match) { + var off = Time.getTimezoneOffset() + , hourOff = off / 60 + , hourDiff = Math.floor(hourOff) + , hours = 18 - hourDiff + , padSpace24 = hours < 10 ? ' ' : '' + , padZero24 = hours < 10 ? '0' : '' + , hour24 = String(hours) + , padSpace12 = (hours % 12) < 10 ? ' ' : '' + , padZero12 = (hours % 12) < 10 ? '0' : '' + , hour12 = String(hours % 12) + , sign = hourDiff < 0 ? '+' : '-' + , minDiff = Time.getTimezoneOffset() - (hourDiff * 60) + , mins = String(51 - minDiff) + , tz = match[1] + , ampm = hour12 == hour24 ? 'AM' : 'PM' + , R = hour24 + ':' + mins + , r = padZero12 + hour12 + ':' + mins + ':45 ' + ampm + , T = R + ':45' + assert.format('%H', padZero24 + hour24, '18') + assert.format('%I', padZero12 + hour12, '06') + assert.format('%k', padSpace24 + hour24, '18') + assert.format('%l', padSpace12 + hour12, ' 6') + assert.format('%M', mins) + assert.format('%P', ampm.toLowerCase(), 'pm') + assert.format('%p', ampm, 'PM') + assert.format('%R', R, '18:51') + assert.format('%r', r, '06:51:45 PM') + assert.format('%T', T, '18:51:45') + assert.format('%Z', tz, 'GMT') + assert.format('%z', sign + '0' + Math.abs(hourDiff) + '00', '+0000') + } +}