1 /**
  2   SC.DateFormatter is used with String.fmt to format dates with a format
  3   string. For example:
  4 
  5       "My date: %{date:yyyy} %{date:MM} %{date:dd}".fmt(myDate, SC.DateFormatter)
  6 
  7   You can use almost any of the Unicode Technical Standard #35 (draft 10)
  8   formatting strings.  See:
  9 
 10     http://unicode.org/reports/tr35/tr35-10.html#Date_Format_Patterns
 11 
 12   Note that you can only put one (yyyy, MM, dd) per token, but you can put as
 13   many tokens as you'd like.
 14 
 15 */
 16 SC.DateFormatter = function(date, f) {
 17   if (!date) {
 18     throw new Error("No date passed to date formatter.");
 19   } else if (!date.getFullYear) {
 20     throw new Error("Object passed to date formatter was not a date!");
 21   }
 22   
 23   // f is expected to be a character, potentially repeated.
 24   // What we do is figure out what letter, and how many times it is repeated.
 25   // we also sanity-check.
 26   if (!f) {
 27     throw new Error("No formatting string passed to date formatter. Date: " + date);
 28   }
 29   
 30   var len = f.length, first = f[0], idx;
 31   for (idx = 1; idx < len; idx++) {
 32     if (f[idx] !== first) {
 33       throw new Error("Invalid format string for a date; all characters must be the same: " + f + "; date: " + date);
 34     }
 35   }
 36   
 37   var formatter = SC.DateFormatter[first];
 38   if (!formatter) {
 39     throw new Error("No formatter `" + first + "` exists for date: " + date);
 40   }
 41   
 42   return formatter(date, len);
 43 };
 44 
 45 //
 46 // Era
 47 //
 48 SC.DateFormatter.G = function(date, count) {
 49   var era = "SC.Date.Era.";
 50   era += date.getFullYear() >= 0 ? "AD" : "BC";
 51   
 52   if (count <= 3) {
 53     // Abbreviated era (AD, BC, etc.)
 54     return (era + ".Abbreviated").loc();
 55   } else if (count === 4) {
 56     return (era + ".Full").loc();
 57   } else if (count === 5) {
 58     return (era + ".Letter").loc();
 59   } else {
 60     throw new Error("Invalid era format: expected at most 5 digits; found " + count + ".");
 61   }
 62 };
 63 
 64 //
 65 // Year
 66 //
 67 SC.DateFormatter.y = function(date, count) {
 68   // this is expected to be the year not accounting for AD/BC.
 69   // JavaScript stores it as a negative for BC years, so we need to
 70   // do a Math.abs()
 71   var year = Math.abs(date.getFullYear()).toString();
 72   while (year.length < count) { year = '0' + year; }
 73   year = year.substr(year.length - count);
 74   return year;
 75 };
 76 
 77 // We only support gregorian calendars, so YYYY would mean the same as yyyy
 78 SC.DateFormatter.Y = function(date, count) {
 79   return SC.DateFormatter.y(date, count);
 80 };
 81 
 82 // u just doesn't do Math.abs
 83 SC.DateFormatter.u = function(date, count) {
 84   var lt0 = date.getFullYear() < 0;
 85   var year = Math.abs(date.getFullYear()).toString();
 86   
 87   while (year.length < count) { year = '0' + year; }
 88   year = year.substr(year.length - count);
 89   
 90   return (lt0 ? "-" : "") + year;
 91 };
 92 
 93 //
 94 // Quarter
 95 //
 96 // I am not overly sure what "standAlone" is, but I think it may be for those
 97 // cases where the month is "standalone" and therefore should be capitalized,
 98 // and such...
 99 SC.DateFormatter.Q = function(date, count, isStandAlone) {
100   var month = date.getMonth(),
101       quarter = Math.floor(month / 3) + 1,
102       quarterName = "SC.Date.Quarter." + (isStandAlone ? "StandAlone." : "") + 
103         "Q" + quarter;
104   
105   if (count === 1) {
106     return "" + quarter;
107   } else if (count === 2) {
108     return "0" + quarter;
109   } else if (count === 3) {
110     return (quarterName + ".Abbreviated").loc();
111   } else if (count == 4) {
112     return (quarterName + ".Full").loc();
113   } else {
114     throw new Error("Unrecognized number of characters for quarter: " + count);
115   }
116 };
117 
118 SC.DateFormatter.q = function(date, count) {
119   return SC.DateFormatter.Q(date, count, YES);
120 };
121 
122 //
123 // Month
124 //
125 
126 // It is a bit easier to translate an english month name to another language
127 // than to translate a number like 0, 1, 2, etc.--especially because you have
128 // no idea, as a translator: are we beginning at 0, or at 1?
129 SC.DateFormatter.ENGLISH_MONTH_NAMES = [
130   "January", "February", "March", 
131   "April", "May", "June",
132   "July", "August", "September",
133   "October", "November", "December"
134 ];
135 
136 SC.DateFormatter.M = function(date, count, isStandAlone) {
137   
138   var month = date.getMonth(),
139       monthString = "" + (month + 1),
140       monthName = "SC.Date.Month." + (isStandAlone ? "StandAlone." : "") + 
141         SC.DateFormatter.ENGLISH_MONTH_NAMES[month];
142   
143   if (count === 1) {
144     return monthString;
145   } else if (count === 2) {
146     if (monthString.length < 2) monthString = "0" + monthString;
147     return monthString;
148   } else if (count === 3) {
149     return (monthName + ".Abbreviated").loc();
150   } else if (count === 4) {
151     return (monthName + ".Full").loc();
152   } else if (count === 5) {
153     return (monthName + ".Letter").loc();
154   } else {
155     throw new Error("The number of Ms or Ls must be from 1 to 5. Supplied: " + count);
156   }
157 };
158 
159 SC.DateFormatter.L = function(date, count) {
160   return SC.DateFormatter.M(date, count, YES);
161 };
162 
163 // OMITTED: l; used only with Chinese calendar.
164 SC.DateFormatter.l = function() { throw new Error("`l` date formatter not implemented."); };
165 
166 //
167 // Omitted for now: Week Number
168 //
169 SC.DateFormatter.w = function(date, count) {
170   throw new Error("Week number currently not supported for date formatting.");
171 };
172 
173 SC.DateFormatter.W = function(date, count) {
174   throw new Error("Week number currently not supported for date formatting.");
175 };
176 
177 //
178 // Day
179 //
180 SC.DateFormatter.d = function(date, count) {
181   // day of month
182   var dayString = "" + date.getDate();
183   if (count > dayString.length) dayString = "0" + dayString;
184   return dayString;
185 };
186 
187 SC.DateFormatter.D = function(date, count) {
188   // day of year
189   var firstDayOfYear = new Date(date.getFullYear(), 0, 1),
190       timestamp = firstDayOfYear.getTime(),
191       diff = date.getTime() - timestamp,
192       days = Math.floor(diff / (24 * 60 * 60 * 1000)) + 1;
193   
194   days = "" + days;
195   while (days.length < count) days = "0" + days;
196   return days;
197 };
198 
199 SC.DateFormatter.F = function(date, count) {
200   // day of week in month; for instance, 2 if it is the second wednesday in the month.
201   throw new Error("Day of week in month (F) is not supported in date formatting");
202 };
203 
204 SC.DateFormatter.g = function(date, count) {
205   // Julian day, modified: should be based on local timezone,
206   // and, more than that, local timezone midnight (not noon)
207   throw new Error("Julian day not supported in date formatting.");
208 };
209 
210 //
211 // Week Day
212 //
213 
214 // See discussion of using a mapping to english names for months...
215 SC.DateFormatter.ENGLISH_DAY_NAMES = [
216   // JavaScript starts week on Sunday
217   "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
218 ];
219 
220 
221 SC.DateFormatter.E = function(date, count) {
222   // E is like e, except that it doesn't ever create a day-of-week number,
223   // and therefore, if the count is less than three, it should be coerced
224   // up to three, because it means the same thing (short day)
225   if (count < 3) count = 3;
226   return SC.DateFormatter.e(date, count);
227 };
228 
229 SC.DateFormatter.e = function(date, count, isStandAlone) {
230   var day = date.getDay(),
231       dayString = "" + (day + 1),
232       dayName = "SC.Date.Day." + (isStandAlone ? "StandAlone." : "") +
233         SC.DateFormatter.ENGLISH_DAY_NAMES[day];
234   
235   if (count === 1) {
236     return dayString;
237   } else if (count === 2) {
238     dayString = "0" + dayString;
239     return dayString;
240   } else if (count === 3) {
241     return (dayName + ".Abbreviated").loc();
242   } else if (count === 4) {
243     return (dayName + ".Full").loc();
244   } else if (count === 5) {
245     return (dayName + ".Letter").loc();
246   } else {
247     throw new Error("Unrecognized number of `e`s, `c`s, or `E`s in date format string.");
248   }
249 };
250 
251 SC.DateFormatter.c = function(date, count) {
252   return SC.DateFormatter.e(date, count, YES);
253 };
254 
255 
256 
257 //
258 // Period
259 //
260 SC.DateFormatter.a = function(date, count) {
261   if (count !== 1) {
262     throw new Error("`a` can only be included in a date format string once.");
263   }
264   
265   var name = "SC.Date.Period." + (date.getHours() > 11 ? "PM" : "AM");
266   return name.loc();
267 };
268 
269 //
270 // Hour
271 //
272 // upper-case: 0-based. WEIRD PART: upper-case H and lower-case k are
273 // 24 hour. WTF? This makes NO SENSE AT ALL!
274 SC.DateFormatter._h = function(date, count, is24, base) {
275   var hour = date.getHours();
276   if (!is24) hour = hour % 12;
277   if (base) {
278     if (!is24) {
279       // if hour is 0, then we need to make it 12.
280       if (hour === 0) hour = 12;
281     } else {
282       // if hour is 0, hour needs to be 24
283       if (hour === 0) hour = 24;
284     }
285   }
286   
287   var hourStr = "" + hour;
288   if (hourStr.length < count) hourStr = "0" + hourStr;
289   
290   return hourStr;
291 };
292 
293 SC.DateFormatter.h = function(date, count) {
294   return SC.DateFormatter._h(date, count, NO, 1);
295 };
296 
297 SC.DateFormatter.H = function(date, count) {
298   return SC.DateFormatter._h(date, count, YES, 0);
299 };
300 
301 SC.DateFormatter.K = function(date, count) {
302   return SC.DateFormatter._h(date, count, NO, 0);
303 };
304 
305 SC.DateFormatter.k = function(date, count) {
306   return SC.DateFormatter._h(date, count, YES, 1);
307 };
308 
309 //
310 // Minute
311 //
312 SC.DateFormatter.m = function(date, count) {
313   var str = "" + date.getMinutes();
314   if (str.length < count) str = "0" + str;
315   return str;
316 };
317 
318 SC.DateFormatter.s = function(date, count) {
319   var str = "" + date.getSeconds();
320   if (str.length < count) str = "0" + str;
321   return str;
322 };
323 
324 SC.DateFormatter.S = function(date, count) {
325   var fraction = date.getMilliseconds() / 1000.0,
326       mult = Math.pow(10, count);
327   
328   fraction = Math.round(fraction * mult);
329   fraction = "" + fraction;
330   
331   while(fraction.length < count) fraction="0" + fraction;
332   return fraction;
333 };
334 
335 SC.DateFormatter.A = function(date, count) {
336   // ms in day...
337   var timestamp = new Date(date.getFullYear(), date.getMonth(), date.getDay()).getTime();
338   var res = date.getTime() - timestamp;
339   res = "" + res;
340   while (res.length < count) res = "0" + res;
341   return res;
342 };
343 
344 SC.DateFormatter.z = 
345 SC.DateFormatter.Z = 
346 SC.DateFormatter.v = 
347 SC.DateFormatter.V = function(date, count) {
348   throw new Error("Timezone not supported in date format strings.");
349 };
350 
351 
352