/**
* The DateHelper module
* a DateHelper class
* @module dateHelper
* @copyright CHECKROOM NV 2015
*/
define(["jquery", "moment"], /** @lends DateHelper */ function ($, moment) {
// Add a new function to moment
moment.fn.toJSONDate = function() {
// toISOString gives the time in Zulu timezone
// we want the local timezone but in ISO formatting
return this.format("YYYY-MM-DD[T]HH:mm:ss.000[Z]");
};
// https://github.com/moment/moment/pull/1595
//m.roundTo('minute', 15); // Round the moment to the nearest 15 minutes.
//m.roundTo('minute', 15, 'up'); // Round the moment up to the nearest 15 minutes.
//m.roundTo('minute', 15, 'down'); // Round the moment down to the nearest 15 minutes.
moment.fn.roundTo = function(units, offset, midpoint) {
units = moment.normalizeUnits(units);
offset = offset || 1;
var roundUnit = function(unit) {
switch (midpoint) {
case 'up':
unit = Math.ceil(unit / offset);
break;
case 'down':
unit = Math.floor(unit / offset);
break;
default:
unit = Math.round(unit / offset);
break;
}
return unit * offset;
};
switch (units) {
case 'year':
this.year(roundUnit(this.year()));
break;
case 'month':
this.month(roundUnit(this.month()));
break;
case 'week':
this.weekday(roundUnit(this.weekday()));
break;
case 'isoWeek':
this.isoWeekday(roundUnit(this.isoWeekday()));
break;
case 'day':
this.day(roundUnit(this.day()));
break;
case 'hour':
this.hour(roundUnit(this.hour()));
break;
case 'minute':
this.minute(roundUnit(this.minute()));
break;
case 'second':
this.second(roundUnit(this.second()));
break;
default:
this.millisecond(roundUnit(this.millisecond()));
break;
}
return this;
};
/*
useHours = BooleanField(default=True)
avgCheckoutHours = IntField(default=4)
roundMinutes = IntField(default=15)
roundType = StringField(default="nearest", choices=ROUND_TYPE) # nearest, longer, shorter
*/
var INCREMENT = 15,
START_OF_DAY_HRS = 9,
END_OF_DAY_HRS = 17;
/**
* @name DateHelper
* @class
* @constructor
*/
var DateHelper = function(spec) {
spec = spec || {};
this.roundType = spec.roundType || "nearest";
this.roundMinutes = spec.roundMinutes || INCREMENT;
this.timeFormat24 = (spec.timeFormat24) ? spec.timeFormat24 : false;
this._momentFormat = (this.timeFormat24) ? "MMM D [at] H:mm" : "MMM D [at] h:mm a";
this.startOfDayHours = (spec.startOfDayHours!=null) ? startOfDayHours : START_OF_DAY_HRS;
this.endOfDayHours = (spec.endOfDayHours!=null) ? endOfDayHours : END_OF_DAY_HRS;
};
/**
* @name parseDate
* @method
* @param data
* @returns {moment}
*/
DateHelper.prototype.parseDate = function(data) {
if (typeof data == 'string' || data instanceof String) {
// "2014-04-03T12:15:00+00:00" (length 25)
// "2014-04-03T09:32:43.841000+00:00" (length 32)
if (data.endsWith('+00:00')) {
var len = data.length;
if (len==25) {
return moment(data.substring(0, len-6));
} else if (len==32) {
return moment(data.substring(0, len-6).split('.')[0]);
}
}
}
};
/**
* @name DateHelper#getNow
* @method
* @return {moment}
*/
DateHelper.prototype.getNow = function() {
// TODO: Use the right MomentJS constructor
// This one will be deprecated in the next version
return moment();
};
/**
* @name DateHelper#getFriendlyDuration
* @method
* @param duration
* @return {}
*/
DateHelper.prototype.getFriendlyDuration = function(duration) {
return duration.humanize();
};
/**
* @name DateHelper#getFriendlyDateParts
* @param date
* @param now (optional)
* @param format (optional)
* @returns [date string,time string]
*/
DateHelper.prototype.getFriendlyDateParts = function(date, now, format) {
/*
moment().calendar() shows friendlier dates
- when the date is <=7d away:
- Today at 4:15 PM
- Yesterday at 4:15 PM
- Last Monday at 4:15 PM
- Wednesday at 4:15 PM
- when the date is >7d away:
- 07/25/2015
*/
if (!moment.isMoment(date)) {
date = moment(date);
}
now = now || this.getNow();
return date.calendar()
.replace("AM", "am")
.replace("PM", "pm")
.split(" at ");
};
/**
* Returns a number of friendly date ranges with a name
* Each date range is a standard transaction duration
* @name getDateRanges
* @method
* @param avgHours
* @param numRanges
* @param now
* @param i18n
* @returns {Array} [{counter: 1, from: m(), to: m(), hours: 24, option: 'days', title: '1 Day'}, {...}]
*/
DateHelper.prototype.getDateRanges = function(avgHours, numRanges, now, i18n) {
if( (now) &&
(!moment.isMoment(now))) {
now = moment(now);
}
i18n = i18n || {
year: "year",
years: "years",
month: "month",
months: "months",
week: "week",
weeks: "weeks",
day: "day",
days: "days",
hour: "hour",
hours: "hours"
};
var timeOptions = ["years", "months", "weeks", "days", "hours"],
timeHourVals = [365*24, 30*24, 7*24, 24, 1],
opt = null,
val = null,
title = null,
counter = 0,
chosenIndex = -1,
ranges = [];
// Find the range kind that best fits the avgHours (search from long to short)
for (var i=0;i<timeOptions.length;i++) {
val = timeHourVals[i];
opt = timeOptions[i];
if (avgHours>=val) {
if( (avgHours % val) == 0) {
chosenIndex = i;
break;
}
}
}
now = now || this.getNow();
if (chosenIndex>=0) {
for(var i=1;i<=numRanges;i++){
counter = i * avgHours;
title = i18n[(counter==1) ? opt.replace("s", "") : opt];
ranges.push({
option: opt,
hours: counter,
title: (counter / timeHourVals[chosenIndex]) + " " + title,
from: now.clone(),
to: now.clone().add(counter, "hours")
})
}
}
return ranges;
};
/**
* getFriendlyFromTo
* returns {fromDate:"", fromTime: "", fromText: "", toDate: "", toTime: "", toText: "", text: ""}
* @param from
* @param to
* @param useHours
* @param now
* @param separator
* @param format
* @returns {}
*/
DateHelper.prototype.getFriendlyFromTo = function(from, to, useHours, now, separator, format) {
if (!moment.isMoment(from)) {
from = moment(from);
}
if (!moment.isMoment(to)) {
to = moment(to);
}
now = now || this.getNow();
var sep = separator || " - ",
fromParts = this.getFriendlyDateParts(from, now, format),
toParts = this.getFriendlyDateParts(to, now, format),
result = {
dayDiff: from ? from.clone().startOf('day').diff(to, 'days') : -1,
fromDate: from ? fromParts[0] : "No from date set",
fromTime: (useHours && from != null) ? fromParts[1] : "",
toDate: to ? toParts[0] : "No to date set",
toTime: (useHours && to != null) ? toParts[1] : ""
};
result.fromText = result.fromDate;
result.toText = result.toDate;
if (useHours) {
if(result.fromTime){
result.fromText += ' ' + result.fromTime;
}
if(result.toTime){
result.toText += ' ' + result.toTime;
}
}
// Build a text based on the dates and times we have
if (result.dayDiff==0) {
if (useHours) {
result.text = result.fromText + sep + result.toText;
} else {
result.text = result.fromText;
}
} else {
result.text = result.fromText + sep + result.toText;
}
return result;
};
/**
* @deprecated use getFriendlyFromToInfo
* [getFriendlyFromToOld]
* @param fromDate
* @param toDate
* @param groupProfile
* @return {}
*/
DateHelper.prototype.getFriendlyFromToOld = function(fromDate, toDate, groupProfile) {
var mFrom = this.roundFromTime(fromDate, groupProfile);
var mTo = this.roundToTime(toDate, groupProfile);
return {
from: mFrom,
to: mTo,
daysBetween: mTo.clone().startOf('day').diff(mFrom.clone().startOf('day'), 'days'),
duration: moment.duration(mFrom - mTo).humanize(),
fromText: mFrom.calendar().replace(' at ', ' '),
toText: mTo.calendar().replace(' at ', ' ')
};
};
/**
* makeStartDate helps making an start date for a transaction, usually a reservation
* It will do the standard rounding
* But also, if you're using dates instead of datetimes,
* it will try to make smart decisions about which hours to use
* @param m - the Moment date
* @param useHours - does the profile use hours?
* @param dayMode - did the selection happen via calendar day selection? (can be true even if useHours is true)
* @param minDate - passing in a minimum start-date, will be different for reservations compared to orders
* @param maxDate - passing in a maximum start-date (not used for the moment)
* @param now - the current moment (just to make testing easier)
* @returns {moment}
* @private
*/
DateHelper.prototype.makeStartDate = function(m, useHours, dayMode, minDate, maxDate, now) {
useHours = (useHours!=null) ? useHours : true; // is the account set up to use hours?
dayMode = (dayMode!=null) ? dayMode : false; // did the selection come from a calendar fullcalendar day selection?
now = now || moment(); // the current time (for unit testing)
if( (useHours) &&
(!dayMode)) {
// The account is set up to use hours,
// and the user picked the hours himself (since it's not picked from a dayMode calendar)
// We'll just round the from date
// if it's before the minDate, just take the minDate instead
m = this.roundTimeFrom(m);
} else {
// When we get here we know that either:
// 1) The account is set up to use hours BUT the date came from a calendar selection that just chose the date part
// or
// 2) The account is set up to use days instead of hours
//
// Which means we still need to see if we can make a smart decision about the hours part
// we'll base this on typical business hours (usually 9 to 5)
var isToday = m.isSame(now, 'day'),
startOfBusinessDay = this._makeStartOfBusinessDay(m);
if (isToday) {
// The start date is today
// and the current time is still before business hours
// we can use the start time to start-of-business hours
if (m.isBefore(startOfBusinessDay)) {
m = startOfBusinessDay;
} else {
// We're already at the beginning of business hours
// or even already passed it, just try rounding the
// time and see if its before minDate
m = this.roundTimeFrom(m);
}
} else {
// The start date is not today, we can just take the business day start from the date that was passed
m = startOfBusinessDay;
}
}
// Make sure we never return anything before the mindate
return ((minDate) && (m.isBefore(minDate))) ? minDate : m;
};
/**
* makeEndDate helps making an end date for a transaction
* It will do the standard rounding
* But also, if you're using dates instead of datetimes,
* it will try to make smart decisions about which hours to use
* @param m - the Moment date
* @param useHours - does the profile use hours?
* @param dayMode - did the selection happen via calendar day selection? (can be true even if useHours is true)
* @param minDate
* @param maxDate
* @param now - the current moment (just to make testing easier)
* @returns {moment}
* @private
*/
DateHelper.prototype.makeEndDate = function(m, useHours, dayMode, minDate, maxDate, now) {
useHours = (useHours!=null) ? useHours : true;
dayMode = (dayMode!=null) ? dayMode : false;
now = now || moment();
if( (useHours) &&
(!dayMode)) {
// The account is set up to use hours,
// and since dayMode is false,
// we assume the hours are picked by the user himself
// just do the rounding and we're done
m = this.roundTimeTo(m);
} else {
// When we get here we know that either:
// 1) The account is set up to use hours BUT the date came from a calendar selection that just chose the date part
// or
// 2) The account is set up to use days instead of hours
//
// Which means we still need to see if we can make a smart decision about the hours part
var isToday = m.isSame(now, 'day'),
endOfBusinessDay = this._makeEndOfBusinessDay(m),
endOfDay = this._makeEndOfDay(m);
if (isToday) {
// The end date is today
// and the current date is before business hours
// we can use the end time to end-of-business hours
if (m.isBefore(endOfBusinessDay)) {
m = endOfBusinessDay;
} else {
m = endOfDay;
}
} else if (m.isAfter(endOfBusinessDay)) {
m = endOfDay;
} else {
m = endOfBusinessDay;
}
}
// Make sure we never return a date after the max date
return ((maxDate) && (m.isAfter(maxDate))) ? maxDate : m;
};
/**
* [getFriendlyDateText]
* @param date
* @param useHours
* @param now
* @param format
* @return {string}
*/
DateHelper.prototype.getFriendlyDateText = function(date, useHours, now, format) {
if (date==null) {
return "Not set";
}
var parts = this.getFriendlyDateParts(date, now, format);
return (useHours) ? parts.join(" ") : parts[0];
};
/**
* [addAverageDuration]
* @param m
* @returns {moment}
*/
DateHelper.prototype.addAverageDuration = function(m) {
// TODO: Read the average order duration from the group.profile
// add it to the date that was passed
return m.clone().add(1, 'day');
};
/**
* roundTimeFrom uses the time rounding rules to round a begin datetime
* @name DateHelper#roundTimeFrom
* @method
* @param m
*/
DateHelper.prototype.roundTimeFrom = function(m) {
return (this.roundMinutes<=1) ? m : this.roundTime(m, this.roundMinutes, this._typeToDirection(this.roundType, "from"));
};
/**
* roundTimeTo uses the time rounding rules to round an end datetime
* @name DateHelper#roundTimeTo
* @method
* @param m
*/
DateHelper.prototype.roundTimeTo = function(m) {
return (this.roundMinutes<=1) ? m : this.roundTime(m, this.roundMinutes, this._typeToDirection(this.roundType, "to"));
};
/**
* @name DateHelper#roundTime
* @method
* @param m
* @param inc
* @param direction
*/
DateHelper.prototype.roundTime = function(m, inc, direction) {
var mom = (moment.isMoment(m)) ? m : moment(m);
mom.seconds(0).milliseconds(0);
return mom.roundTo("minute", inc || INCREMENT, direction);
};
/**
* @name DateHelper#roundTimeUp
* @method
* @param m
* @param inc
*/
DateHelper.prototype.roundTimeUp = function(m, inc) {
var mom = (moment.isMoment(m)) ? m : moment(m);
mom.seconds(0).milliseconds(0);
return mom.roundTo("minute", inc || INCREMENT, "up");
};
/**
* @name DateHelper#roundTimeDown
* @method
* @param m
* @param inc
*/
DateHelper.prototype.roundTimeDown = function(m, inc) {
var mom = (moment.isMoment(m)) ? m : moment(m);
mom.seconds(0).milliseconds(0);
return mom.roundTo("minute", inc || INCREMENT, "down");
};
DateHelper.prototype._typeToDirection = function(type, fromto) {
switch(type) {
case "longer":
switch(fromto) {
case "from": return "down";
case "to": return "up";
default: break;
}
break;
case "shorter":
switch(fromto) {
case "from": return "up";
case "to": return "down";
default: break;
}
break;
default:
break;
}
};
DateHelper.prototype._makeStartOfBusinessDay = function(m) {
return m.clone().hours(this.startOfDayHours).minutes(0).seconds(0).milliseconds(0);
};
DateHelper.prototype._makeEndOfBusinessDay = function(m) {
return m.clone().hours(this.endOfDayHours).minutes(0).seconds(0).milliseconds(0);
};
DateHelper.prototype._makeEndOfDay = function(m) {
return m.clone().hours(23).minutes(45).seconds(0).milliseconds(0);
};
return DateHelper;
});