From f0052d3e3c5bfd2e581cda3c5e84f3861b63014a Mon Sep 17 00:00:00 2001 From: rolux Date: Thu, 26 May 2011 19:10:32 +0200 Subject: [PATCH] much better formatting of date ranges and their duration --- demos/calendar/js/calendar.js | 5 + demos/test/js/test.js | 3 - source/Ox.UI/js/Calendar/Ox.Calendar.js | 100 ++++------ source/Ox.js | 236 +++++++++++++++++++++--- 4 files changed, 247 insertions(+), 97 deletions(-) diff --git a/demos/calendar/js/calendar.js b/demos/calendar/js/calendar.js index b76dab61..577269b1 100644 --- a/demos/calendar/js/calendar.js +++ b/demos/calendar/js/calendar.js @@ -36,14 +36,19 @@ Ox.load('UI', {debug: true, hideScreen: true, showScreen: true, theme: 'modern'} {name: 'Summer 1968', start: '1968-06', end: '1968-09', type: 'date'}, {name: '1969', start: '1969', end: '1970', type: 'date'}, {name: '1970s', start: '1970', end: '1980', type: 'date'}, + {name: '1970', start: '1970', end: '1971', type: 'date'}, {name: '1980s', start: '1980', end: '1990', type: 'date'}, {name: '1990s', start: '1990', end: '2000', type: 'date'}, + {name: '3rd Millennium', start: '2000', end: '3000', type: 'date'}, {name: '21st Century', start: '2000', end: '2100', type: 'date'}, {name: '2000s', start: '2000', end: '2010', type: 'date'}, + {name: '2000', start: '2000', end: '2001', type: 'date'}, + {name: '2001', start: '2001', end: '2002', type: 'date'}, {name: '2010s', start: '2010', end: '2020', type: 'date'}, {name: '2020s', start: '2020', end: '2030', type: 'date'}, + {name: 'Julius Caesar', start: '-100-07-13', end: '-44-03-15', type: 'person'}, {name: 'Barbarossa', start: '1122', end: '1190-06-10', type: 'person'}, {name: 'Genghis Khan', start: '1162', end: '1228', type: 'person'}, {name: 'Marco Polo', start: '1254', end: '1324-01-08', type: 'person'}, diff --git a/demos/test/js/test.js b/demos/test/js/test.js index 2233516a..dbbc63c3 100644 --- a/demos/test/js/test.js +++ b/demos/test/js/test.js @@ -95,9 +95,6 @@ Ox.load('UI', { WebkitUserSelect: 'text' }); ['moz', 'webkit'].forEach(function(browser) { - Ox.print('-' + browser + '-linear-gradient(left top, left bottom, rgb(' + - getColor(success, 0) + '), rgb(' + - getColor(success, 1) + '))') $div.css({ background: '-' + browser + '-linear-gradient(top, rgb(' + getColor(success, 0) + '), rgb(' + diff --git a/source/Ox.UI/js/Calendar/Ox.Calendar.js b/source/Ox.UI/js/Calendar/Ox.Calendar.js index ed4fce99..ffa60f17 100644 --- a/source/Ox.UI/js/Calendar/Ox.Calendar.js +++ b/source/Ox.UI/js/Calendar/Ox.Calendar.js @@ -74,12 +74,26 @@ Ox.Calendar = function(options, self) { self.options.events.forEach(function(event) { event.id = Ox.isUndefined(event.id) ? Ox.uid() : event.id; - event.start = Ox.parseDate(event.start, true); - event.end = Ox.parseDate(event.end, true); + event.startTime = Ox.parseDate(event.start, true); + event.endTime = Ox.parseDate(event.end, true); + event.rangeText = Ox.formatDateRange(event.start, event.end, true); + event.durationText = Ox.formatDateRangeDuration(event.start, event.end, true); }); self.maxZoom = 32; self.minLabelWidth = 80; + + /* + We need to iterate over irregular intervals, like months or years. + The idea is to put this logic into a data structure, the units. + Just like the 0-th second is 1970-01-01 00:00:00, the 0th month + is 1970-01, or the 0-th century is the 20th century. + A month unit, for example, has the following properties: + - seconds: average number of seconds + - date: returns the start date of the index-th month + - name: returns a string representation of the index-th month + - value: returns the month index for a given date + */ self.units = [ { id: 'millennium', @@ -93,8 +107,8 @@ Ox.Calendar = function(options, self) { }, name: function(i) { return i > -2 - ? Ox.formatOrdinal(i + 2) + ' millennium' - : Ox.formatOrdinal(-i - 1) + ' millennium BC' + ? Ox.formatOrdinal(i + 2) + ' Millennium' + : Ox.formatOrdinal(-i - 1) + ' Millennium BC' }, value: function(date) { return Math.floor(date.getUTCFullYear() / 1000) - 1; @@ -112,8 +126,8 @@ Ox.Calendar = function(options, self) { }, name: function(i) { return i > -20 - ? Ox.formatOrdinal(i + 20) + ' century' - : Ox.formatOrdinal(-i - 19) + ' century BC' + ? Ox.formatOrdinal(i + 20) + ' Century' + : Ox.formatOrdinal(-i - 19) + ' Century BC' }, value: function(date) { return Math.floor(date.getUTCFullYear() / 100) - 19; @@ -506,55 +520,11 @@ Ox.Calendar = function(options, self) { renderCalendar(); } - function formatEvent(event) { - return formatEventRange(event) + '
' + formatEventDuration(event); - } - - function formatEventDuration(event) { - // fixme: still wrong because of different number of leap days - var date = new Date(getEventDuration(event)), - strings = [], - values = { - years: date.getUTCFullYear() - 1970, - days: Ox.getDayOfTheYear(date, true) - 1, - hours: date.getUTCHours(), - minutes: date.getUTCMinutes(), - seconds: date.getUTCMilliseconds() - }; - //Ox.print('****', values); - ['year', 'day', 'hour', 'minute', 'second'].forEach(function(key) { - var value = values[key + 's']; - value && strings.push(value + ' ' + key + (value > 1 ? 's' : '')); - }); - return strings.join(' '); - } - - function formatEventRange(event) { - var isFullDays = Ox.formatDate(event.start, '%H:%M:%S', true) == '00:00:00' && - Ox.formatDate(event.end, '%H:%M:%S', true) == '00:00:00', - isOneDay = isFullDays && getEventDuration(event) == 86400000, // fixme: wrong, DST - isSameDay = Ox.formatDate(event.start, '%Y-%m-%d', true) == - Ox.formatDate(event.end, '%Y-%m-%d', true), - isSameYear = event.start.getUTCFullYear() == event.end.getUTCFullYear(), - timeFormat = isFullDays ? '' : ', %H:%M:%S', - string = Ox.formatDate(event.start, '%a, %b %e', true); - if (isOneDay || isSameDay || !isSameYear) { - string += Ox.formatDate(event.start, ', %Y' + timeFormat, true); - } - if (!isOneDay && !isSameDay) { - string += Ox.formatDate(event.end, ' - %a, %b %e, %Y' + timeFormat, true); - } - if (isSameDay) { - string += Ox.formatDate(event.end, ' - ' + timeFormat.replace(', ', ''), true); - } - return string; - } - function getCalendarEvent(zoom) { var ms = self.options.width * getSecondsPerPixel(zoom) * 1000; return { - start: new Date(+self.options.date - ms / 2), - end: new Date(+self.options.date + ms / 2) + startTime: new Date(+self.options.date - ms / 2), + endTime: new Date(+self.options.date + ms / 2) }; } @@ -570,17 +540,17 @@ Ox.Calendar = function(options, self) { } function getEventCenter(event) { - return new Date(+event.start + getEventDuration(event) / 2); + return new Date(+event.startTime + getEventDuration(event) / 2); } function getEventDuration(event) { - return event.end - event.start; + return event.endTime - event.startTime; } function getEventElement(event, zoom) { - var left = Math.max(getPosition(event.start, zoom), -10000), + var left = Math.max(getPosition(event.startTime, zoom), -10000), paddingLeft = (event.type && left < 0 ? -left : 0), - width = Ox.limit(getPosition(event.end, zoom) - left, 1, 20000) - paddingLeft; + width = Ox.limit(getPosition(event.endTime, zoom) - left, 1, 20000) - paddingLeft; return new Ox.Element() .addClass('OxEvent' + (event.type ? ' Ox' + Ox.toTitleCase(event.type) : '' ) + @@ -692,8 +662,8 @@ Ox.Calendar = function(options, self) { $elements.push( getEventElement({ name: unit.name(value + i), - start: unit.date(value + i), - end: unit.date(value + i + 1) + startTime: unit.date(value + i), + endTime: unit.date(value + i + 1) }, zoom) .addClass(Ox.mod(value + i, 2) == 0 ? 'even' : 'odd') ); @@ -729,7 +699,7 @@ Ox.Calendar = function(options, self) { if ($target.is('.OxLine > .OxEvent')) { event = getEventById($target.data('id')); title = '' + event.name + '
' + - formatEvent(event); + event.rangeText + '
' + event.durationText; } else { title = Ox.formatDate(getMouseDate(e), '%a, %b %e, %Y, %H:%M:%S', true); } @@ -763,9 +733,9 @@ Ox.Calendar = function(options, self) { function overlaps(eventA, eventB) { return ( - eventA.start >= eventB.start && eventA.start < eventB.end + eventA.startTime >= eventB.startTime && eventA.startTime < eventB.endTime ) || ( - eventB.start >= eventA.start && eventB.start < eventA.end + eventB.startTime >= eventA.startTime && eventB.startTime < eventA.endTime ); } @@ -827,10 +797,10 @@ Ox.Calendar = function(options, self) { return -1; } else if (a.type != 'date' && b.type == 'date') { return 1; - } else if (a.start < b.start || a.start > b.start) { - return a.start - b.start; + } else if (a.startTime < b.startTime || a.startTime > b.startTime) { + return a.startTime - b.startTime; } else { - return (b.end - b.start) - (a.end - a.start); + return (b.endTime - b.startTime) - (a.endTime - a.startTime); } }).forEach(function(event, i) { var line = lineEvents.length; @@ -865,7 +835,7 @@ Ox.Calendar = function(options, self) { .appendTo(self.$content); events.sort(function(a, b) { // sort events by start, ascending - return a.start - b.start; + return a.startTime - b.startTime; }).forEach(function(event) { overlaps(event, calendarEvent) && getEventElement(event).appendTo($line); diff --git a/source/Ox.js b/source/Ox.js index 01b7af30..c456275f 100644 --- a/source/Ox.js +++ b/source/Ox.js @@ -1063,8 +1063,11 @@ Ox.rgb = function(hsl) { //@ Ox.AMPM <[str]> ['AM', 'PM'] Ox.AMPM = ['AM', 'PM']; -//@ Ox.DURATIONS <[str]> ['year', 'month', 'day', 'minute', 'second'] -Ox.DURATIONS = ['year', 'month', 'day', 'minute', 'second']; +//@ Ox.BCAD <[str]> ['BC', 'AD'] +Ox.BCAD = ['BC', 'AD']; +// fixme: this is unused, and probably unneeded +//@ Ox.DURATIONS <[str]> ['year', 'month', 'day', 'hour', 'minute', 'second'] +Ox.DURATIONS = ['year', 'month', 'day', 'hour', 'minute', 'second']; //@ Ox.EARTH_RADIUS Radius of the earth in meters // see http://en.wikipedia.org/wiki/WGS-84 Ox.EARTH_RADIUS = 6378137; @@ -1199,8 +1202,6 @@ Ox.getDateInWeek Get the date that falls on a given weekday in the same week @*/ // fixme: why is this Monday first? shouldn't it then be "getDateInISOWeek"?? Ox.getDateInWeek = function(date, weekday, utc) { - /* - */ date = Ox.makeDate(date); Ox.print(date, Ox.getDate(date, utc), Ox.formatDate(date, '%u', utc), date) var sourceWeekday = Ox.getISODay(date, utc), @@ -1208,7 +1209,6 @@ Ox.getDateInWeek = function(date, weekday, utc) { Ox.map(Ox.WEEKDAYS, function(v, i) { return v.substr(0, 3) == weekday.substr(0, 3) ? i + 1 : null; })[0]; - Ox.print(date, Ox.getDate(date, utc), sourceWeekday, targetWeekday) return Ox.setDate(date, Ox.getDate(date, utc) - sourceWeekday + targetWeekday, utc); } @@ -1428,8 +1428,8 @@ Ox.makeDate Takes a date, number or string, returns a date '01/01/1970' @*/ Ox.makeDate = function(date) { - return Ox.isDate(date) ? date : - Ox.isUndefined(date) ? new Date() : new Date(date); + // if date is a date, new Date(date) makes a clone + return Ox.isUndefined(date) ? new Date() : new Date(date); }; /*@ @@ -1445,12 +1445,11 @@ Ox.makeYear = function(date, utc) { return Ox.isDate(date) ? Ox.getFullYear(date, utc) : parseInt(date); }; - /*@ -Ox.parseDate(f) Takes a string ('YYYY-MM-DD HH:MM:SS') and returns a date +Ox.parseDate Takes a string ('YYYY-MM-DD HH:MM:SS') and returns a date str string utc If true, Date is UTC - > +Ox.parseDate('1970-01-01 01:01:01') + > +Ox.parseDate('1970-01-01 01:01:01', true) 3661000 > +Ox.parseDate('1970', true) 0 @@ -1458,9 +1457,9 @@ Ox.parseDate(f) Takes a string ('YYYY-MM-DD HH:MM:SS') and returns a date 50 @*/ Ox.parseDate = function(str, utc) { - var date = new Date(), + var date = new Date(0), defaults = [, 1, 1, 0, 0, 0], - values = /(\d+)-?(\d+)?-?(\d+)? ?(\d+)?:?(\d+)?:?(\d+)?/(str); + values = /(-?\d+)-?(\d+)?-?(\d+)? ?(\d+)?:?(\d+)?:?(\d+)?/(str); values.shift(); values = values.map(function(v, i) { return v || defaults[i]; @@ -1469,11 +1468,29 @@ Ox.parseDate = function(str, utc) { [ 'FullYear', 'Month', 'Date', 'Hours', 'Minutes', 'Seconds' ].forEach(function(part, i) { - date = Ox['set' + part](date, values[i], utc); + Ox['set' + part](date, values[i], utc); }); return date; }; +/* +Ox.parseDateRange = function(start, end, utc) { + var dates = [ + Ox.parseDate(start, utc), + Ox.parseDate(end, utc) + ], + part = [ + 'FullYear', 'Month', 'Date', 'Hours', 'Minutes', 'Seconds' + ][ + Ox.compact( + /(-?\d+)-?(\d+)?-?(\d+)? ?(\d+)?:?(\d+)?:?(\d+)?/(end) + ).length - 2 + ]; + Ox['set' + part](dates[1], Ox['get' + part](dates[1], utc) + 1, utc); + return dates; +}; +*/ + //@ Ox.setDate Set the day of a date, optionally UTC // see Ox.setSeconds for source code //@ Ox.setDay Set the weekday of a date, optionally UTC @@ -1493,14 +1510,15 @@ Ox.parseDate = function(str, utc) { [ 'FullYear', 'Month', 'Date', 'Day', 'Hours', 'Minutes', 'Seconds', 'Milliseconds' -].forEach(function(noun) { - Ox['get' + noun] = function(date, utc) { - return Ox.makeDate(date)['get' + (utc ? 'UTC' : '') + noun]() +].forEach(function(part) { + Ox['get' + part] = function(date, utc) { + return Ox.makeDate(date)['get' + (utc ? 'UTC' : '') + part]() } - Ox['set' + noun] = function(date, num, utc) { - return new Date( - Ox.makeDate(date)['set' + (utc ? 'UTC' : '') + noun](num) - ); + // Ox.setPart(date) modifies date + Ox['set' + part] = function(date, num, utc) { + return ( + Ox.isDate(date) ? date : new Date(date) + )['set' + (utc ? 'UTC' : '') + part](num); } }); @@ -2096,6 +2114,7 @@ Ox.formatDate Formats a date according to a format string See strftime and ISO 8601. + '%Q' (quarter) and '%X' (year with 'BC'/'AD') are non-standard @@ -2169,10 +2188,8 @@ Ox.formatDate Formats a date according to a format string '00' > Ox.formatDate(Ox.test.date, '%w') // Decimal weekday (0-6, Sunday as first day) '0' - > Ox.formatDate(Ox.test.date, '%X') // US time - '12:03:04 AM' - > Ox.formatDate(Ox.test.date, '%x') // US date - '01/02/05' + > Ox.formatDate(Ox.test.date, '%X') // Full year with BC or AD + '2005 AD' > Ox.formatDate(Ox.test.date, '%Y') // Full year '2005' > Ox.formatDate(Ox.test.date, '%y') // Abbreviated year @@ -2192,9 +2209,7 @@ Ox.formatDate = function(date, str, utc) { date = Ox.makeDate(date); var format = [ ['%', function() {return '%{%}';}], - ['c', function() {return '%x %X';}], - ['X', function() {return '%r';}], - ['x', function() {return '%D';}], + ['c', function() {return '%D %r';}], ['D', function() {return '%m/%d/%y';}], ['F', function() {return '%Y-%m-%d';}], ['h', function() {return '%b';}], @@ -2226,9 +2241,15 @@ Ox.formatDate = function(date, str, utc) { ['U', function(d) {return Ox.pad(Ox.getWeek(d, utc), 2);}], ['u', function(d) {return Ox.getISODay(d, utc);}], ['V', function(d) {return Ox.pad(Ox.getISOWeek(d, utc), 2);}], - ['W', function(d) {return Ox.pad(Math.floor((Ox.getDayOfTheYear(d, utc) + - (Ox.getFirstDayOfTheYear(d, utc) || 7) - 2) / 7), 2);}], + ['W', function(d) { + return Ox.pad(Math.floor((Ox.getDayOfTheYear(d, utc) + + (Ox.getFirstDayOfTheYear(d, utc) || 7) - 2) / 7), 2); + }], ['w', function(d) {return Ox.getDay(d, utc);}], + ['X', function(d) { + var y = Ox.getFullYear(d, utc); + return Math.abs(y) + ' ' + Ox.BCAD[y < 0 ? 0 : 1]; + }], ['Y', function(d) {return Ox.getFullYear(d, utc);}], ['y', function(d) {return Ox.getFullYear(d, utc).toString().substr(-2);}], ['Z', function(d) {return d.toString().split('(')[1].replace(')', '');}], @@ -2246,6 +2267,163 @@ Ox.formatDate = function(date, str, utc) { return str; }; + +/*@ +Ox.formatDateRange Formats a date range as a string + A date range is a pair of arbitrary-presicion date strings + > Ox.formatDateRange('2000', '2001') + '2000' + > Ox.formatDateRange('2000', '2002') + '2000 - 2002' + > Ox.formatDateRange('2000-01', '2000-02') + 'January 2000' + > Ox.formatDateRange('2000-01', '2000-03') + 'January - March 2000' + > Ox.formatDateRange('2000-01-01', '2000-01-02') + 'Sat, Jan 1, 2000' + > Ox.formatDateRange('2000-01-01', '2000-01-03') + 'Sat, Jan 1 - Mon, Jan 3, 2000' + > Ox.formatDateRange('2000-01-01 00', '2000-01-01 01') + 'Sat, Jan 1, 2000, 00:00' + > Ox.formatDateRange('2000-01-01 00', '2000-01-01 02') + 'Sat, Jan 1, 2000, 00:00 - 02:00' + > Ox.formatDateRange('2000-01-01 00:00', '2000-01-01 00:01') + 'Sat, Jan 1, 2000, 00:00' + > Ox.formatDateRange('2000-01-01 00:00', '2000-01-01 00:02') + 'Sat, Jan 1, 2000, 00:00 - 00:02' + > Ox.formatDateRange('2000-01-01 00:00:00', '2000-01-01 00:00:01') + 'Sat, Jan 1, 2000, 00:00:00' + > Ox.formatDateRange('2000-01-01 00:00:00', '2000-01-01 00:00:02') + 'Sat, Jan 1, 2000, 00:00:00 - 00:00:02' + > Ox.formatDateRange('-50', '50') + '50 BC - 50 AD' + > Ox.formatDateRange('-50-01-01', '-50-12-31') + 'Sun, Jan 1 - Sun, Dec 31, 50 BC' + > Ox.formatDateRange('-50-01-01 00:00:00', '-50-01-01 23:59:59') + 'Sun, Jan 1, 50 BC, 00:00:00 - 23:59:59' +@*/ +Ox.formatDateRange = function(start, end, utc) { + var isOneUnit = false, + range = [start, end], + strings, + dates = range.map(function(str){ + return Ox.parseDate(str, utc); + }), + parts = range.map(function(str) { + var parts = Ox.compact( + /(-?\d+)-?(\d+)?-?(\d+)? ?(\d+)?:?(\d+)?:?(\d+)?/(str) + ); + parts.shift(); + return parts.map(function(part) { + return parseInt(part); + }); + }), + precision = parts.map(function(parts) { + return parts.length; + }), + y = parts[0][0] < 0 ? '%X' : '%Y', + formats = [ + y, + '%B ' + y, + '%a, %b %e, ' + y, + '%a, %b %e, ' + y + ', %H:%M', + '%a, %b %e, ' + y + ', %H:%M', + '%a, %b %e, ' + y + ', %H:%M:%S', + ]; + if (precision[0] == precision[1]) { + isOneUnit = true; + Ox.loop(precision[0], function(i) { + if (i < precision[0] - 1 && parts[0][i] != parts[1][i]) { + isOneUnit = false; + } + if (i == precision[0] - 1 && parts[0][i] != parts[1][i] - 1) { + isOneUnit = false; + } + return isOneUnit; + }); + } + if (isOneUnit) { + strings = [Ox.formatDate(dates[0], formats[precision[0] - 1], utc)]; + } else { + format = formats[precision[0] - 1]; + strings = [ + Ox.formatDate(dates[0], formats[precision[0] - 1], utc), + Ox.formatDate(dates[1], formats[precision[1] - 1], utc) + ]; + // if same year, and neither date is more precise than day, then omit first year + if ( + parts[0][0] == parts[1][0] + && precision[0] <= 3 + && precision[1] <= 3 + ) { + strings[0] = Ox.formatDate( + dates[0], formats[precision[0] - 1].replace( + new RegExp(',? ' + y), '' + ), utc + ); + } + // if same day then omit second day + if ( + parts[0][0] == parts[1][0] + && parts[0][1] == parts[1][1] + && parts[0][2] == parts[1][2] + ) { + strings[1] = strings[1].split(', ').pop(); + } + } + return strings.map(function(string) { + // %e is a space-padded day + return string.replace(' ', ' '); + }).join(' - '); +}; + +/*@ +Ox.formatDateRangeDuration Formats the duration of a date range as a string + A date range is a pair of arbitrary-presicion date strings + > Ox.formatDateRangeDuration('2000-01-01 00:00:00', '2001-01-03 03:04:05') + '1 year 2 days 3 hours 4 minutes 5 seconds' + > Ox.formatDateRangeDuration('1999', '2000', true) + '1 year' + > Ox.formatDateRangeDuration('2000', '2001', true) + '1 year' + > Ox.formatDateRangeDuration('1999-02', '1999-03', true) + '1 month' + > Ox.formatDateRangeDuration('2000-02', '2000-03', true) + '1 month' +@*/ +Ox.formatDateRangeDuration = function(start, end, utc) { + var date = Ox.parseDate(start, utc), + dates = [start, end].map(function(str) { + return Ox.parseDate(str, utc); + }), + keys = ['year', 'month', 'day', 'hour', 'minute', 'second'], + parts = ['FullYear', 'Month', 'Date', 'Hours', 'Minutes', 'Seconds'], + values = []; + Ox.forEach(keys, function(key, i) { + while (true) { + if (key == 'month') { + Ox.setDate(date, Math.min( + Ox.getDate(date, utc), + Ox.getDaysInMonth( + Ox.getFullYear(date, utc), + Ox.getMonth(date, utc) + 2 + ) + ), utc); + } + Ox['set' + parts[i]](date, Ox['get' + parts[i]](date, utc) + 1, utc); + if (date <= dates[1]) { + values[i] = (values[i] || 0) + 1; + } else { + Ox['set' + parts[i]](date, Ox['get' + parts[i]](date, utc) - 1, utc); + break; + } + } + }); + return Ox.map(values, function(value, i) { + return value ? value + ' ' + keys[i] + (value > 1 ? 's' : '') : null; + }).join(' '); +}; + /*@ Ox.formatDuration Formats a duration as a string > Ox.formatDuration(123456.789, 3)