angular.module("sn.app_common") .service("modelUtil", ['SNAPI', 'TIME', '$rootScope', '$http', '$log', function(SNAPI, TIME, $rootScope, $http, $log) { var modelUtil = this; var DATE_RX = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/; var YMD_RX = /^\d{4}-\d{2}-\d{2}$/; var DEFAULT_GLIDE_LIST_SEPARATOR = ", "; // Mapping of legacy ambiguous time zones to equivalent. Taken from Java mappings // See: https://docs.oracle.com/javase/8/docs/api/java/time/ZoneId.html#SHORT_IDS var SHORT_IDS = { "ACT": "Australia/Darwin", "AET": "Australia/Sydney", "AGT": "America/Argentina/Buenos_Aires", "ART": "Africa/Cairo", "AST": "America/Anchorage", "BET": "America/Sao_Paulo", "BST": "Asia/Dhaka", "CAT": "Africa/Harare", "CNT": "America/St_Johns", "CST": "America/Chicago", "CTT": "Asia/Shanghai", "EAT": "Africa/Addis_Ababa", "ECT": "Europe/Paris", "IET": "America/Indiana/Indianapolis", "IST": "Asia/Kolkata", "JST": "Asia/Tokyo", "MIT": "Pacific/Apia", "NET": "Asia/Yerevan", "NST": "Pacific/Auckland", "PLT": "Asia/Karachi", "PNT": "America/Phoenix", "PRT": "America/Puerto_Rico", "PST": "America/Los_Angeles", "SST": "Pacific/Guadalcanal", "VST": "Asia/Ho_Chi_Minh" }; /** * Returns a valid time zone name compensating for ambiguous time zones. * Defaults to UTC if an unsupported value is provided. */ modelUtil.checkTzName = function(tzName) { if (!tzName) return; // Supported values if (moment.tz.zone(tzName) != null) return tzName; // Ambiguous values if (typeof SHORT_IDS[tzName] !== "undefined") return SHORT_IDS[tzName]; // Unknown values return "UTC"; }; var dstOffset = {}; /** * Get the DST offset for a timezone for a given timestamp. * Takes into account offsets which are not 1 hour, years/locals without DST etc. * * timestamp: UTC timestamp * tzName: name of the timezone. Defaults to local zone if not present. */ modelUtil.getDSTOffset = function(timestamp, tzName) { var year = new Date(timestamp).getFullYear(); tzName = this.checkTzName(tzName); var tzKey = (tzName ? tzName : 'local') + "/" + year; var jan = new Date(year, 0, 1); if (dstOffset[tzKey]) return dstOffset[tzKey]; if (!tzName) { for (var i = 1; i < 12; i++) { dstOffset[tzKey] = Math.abs(jan.getTimezoneOffset() - (new Date(year,i,1)).getTimezoneOffset()); if (dstOffset[tzKey] != 0) break; } return dstOffset[tzKey]; } // Same as above but for a defined timezone. var zone = moment.tz.zone(tzName); for (var j = 1; j < 12; j++) { dstOffset[tzKey] = Math.abs(zone.utcOffset(jan.getTime()) - zone.utcOffset((new Date(year, j, 1)).getTime())); if (dstOffset[tzKey] != 0) break; } return dstOffset[tzKey]; }; /** * Returns the MS difference between the client local time zone offset and the provided time zone offset. * * timestamp: UTC timestamp * tzName: name of the timezone. Defaults to local offset if not present. */ modelUtil.getTzToLocalMSDiff = function(timestamp, tzName) { if (!tzName) return 0; // No difference if no time zone. return (moment.tz.zone(this.checkTzName(tzName)).utcOffset(timestamp) * TIME.MINUTE * -1) - ((new Date(timestamp)).getTimezoneOffset() * TIME.MINUTE * -1); }; /** * Patch object with values from record watcher/table API format * Adds in 'value_ms' UTC millisecond value for recognised dates */ modelUtil.patchObject = function(obj, patch) { for (var attr in patch) { var permission; if (obj[attr]) permission = obj[attr]._canWrite; obj[attr] = patch[attr]; if (permission) obj[attr]._canWrite = permission; modelUtil.addValueMS(obj[attr]); } }; /** * Adds a value_ms to a date time field. */ modelUtil.addValueMS = function(element) { function strToMS(strDate) { var dtComp = strDate.split(" "); var dComp = dtComp[0].split("-"); //y0, m1, d2 var tComp = dtComp[1].split(":"); //h0, mi1, s2 return Date.UTC(dComp[0], dComp[1]-1, dComp[2], tComp[0], tComp[1], tComp[2]); } function strYMDToMS(strYMD) { var dComp = strYMD.split("-"); return Date.UTC(dComp[0], dComp[1]-1, dComp[2]); } // If the value matches a date stamp, add a conversion to MS. if (typeof element.value === "string") { if (element.value.match(DATE_RX)) element.value_ms = strToMS(element.value); else if (element.value.match(YMD_RX)) element.value_ms = strYMDToMS(element.value); else return; //If it's not a date } else return; // If it's not a string // Create a forced TZ display_value_ms if (element.tz_name && element.value_ms) { element.display_value_ms = element.value_ms + modelUtil.getTzToLocalMSDiff(element.value_ms, element.tz_name); // Adjust display value MS for DST if the value and display value span DST changeover var dispInDST = moment(element.display_value_ms).isDST(); var valInDST = moment(element.value_ms).isDST(); if (dispInDST && !valInDST) element.display_value_ms -= (this.getDSTOffset(element.value_ms) * TIME.MINUTE); else if (!dispInDST && valInDST) element.display_value_ms += (this.getDSTOffset(element.value_ms) * TIME.MINUTE); } }; /** * Add a value_ms to all date time fields in record */ modelUtil.addValueMSToAll = function(record) { for (var attr in record) modelUtil.addValueMS(record[attr]); }; /** * Adds from now/to now display value as display_value_friendly */ modelUtil.addFriendlyDisplayValue = function(element) { if (!element.value_ms) modelUtil.addValueMS(element); if (element.value_ms) { // Check if we actually have an ms value if (element.value_ms > Date.now()) element.display_value_friendly = moment(element.value_ms).toNow(); else element.display_value_friendly = moment(element.value_ms).fromNow(); } }; /** * Adds a from now/to now display value to all date elements */ modelUtil.addFriendlyDisplayValueToAll = function(record) { for (var attr in record) modelUtil.addFriendlyDisplayValue(record[attr]); }; /** * Converts a GlideList representation to JS array. */ modelUtil.glideListToArray = function(element) { if(!angular.isArray(element.display_value)) { if (element.display_value && typeof element.display_value === "string") element.display_value = element.display_value.split(DEFAULT_GLIDE_LIST_SEPARATOR); else element.display_value = []; } if(angular.isArray(element.value)) return; if (element.value && typeof element.value === "string") element.value = element.value.split(","); else element.value = []; }; /** * Simple fail function for REST requests */ modelUtil.failNicely = function(response) { $log.info("REST Failure"); $log.info(response); if (!(response.data && response.data.error)) return; //If we have any error info try and get any messages $http.get(SNAPI.SERVICE.NOTIFICATION).then(function(response) { if (response.data.result) response.data.result.forEach(function(err){ $rootScope.$broadcast("$$uiNotification", {type: err.type, message: err.text}); }); }, function(response) { $log.info("Notification Failure"); $log.info(response); }); }; /** * Simple check if id is 32 bytes of valid hexadecimal */ modelUtil.isValidSysID = function(sysId) { var re = /^[a-f0-9]{32}$/i; return re.test(sysId); }; }]);