diff --git a/Core ServiceNow APIs/GlideDateTime/Business time utilities (add, diff, next open, in schedule)/BusinessTimeUtils.js b/Core ServiceNow APIs/GlideDateTime/Business time utilities (add, diff, next open, in schedule)/BusinessTimeUtils.js new file mode 100644 index 0000000000..7f5eeb61d8 --- /dev/null +++ b/Core ServiceNow APIs/GlideDateTime/Business time utilities (add, diff, next open, in schedule)/BusinessTimeUtils.js @@ -0,0 +1,136 @@ +var BusinessTimeUtils = Class.create(); +BusinessTimeUtils.prototype = { + initialize: function() {}, + + /** + * Add working hours to a start date, respecting schedule and holidays. + * @param {String} scheduleSysId - sys_id of the GlideSchedule + * @param {Number} hoursToAdd - working hours to add (can be fractional) + * @param {GlideDateTime|String} startGdt - start time; if string, must be ISO/Glide-friendly + * @param {String} [timeZone] - optional IANA TZ, else schedule/instance TZ + * @returns {Object} { ok:Boolean, due:GlideDateTime|null, minutesAdded:Number, message:String } + */ + addWorkingHours: function(scheduleSysId, hoursToAdd, startGdt, timeZone) { + var result = { ok: false, due: null, minutesAdded: 0, message: '' }; + try { + this._assertSchedule(scheduleSysId); + var start = this._toGdt(startGdt); + var msToAdd = Math.round(Number(hoursToAdd) * 60 * 60 * 1000); + if (!isFinite(msToAdd) || msToAdd <= 0) { + result.message = 'hoursToAdd must be > 0'; + return result; + } + + var sched = new GlideSchedule(scheduleSysId, timeZone || ''); + var due = sched.add(new GlideDateTime(start), msToAdd); // returns GlideDateTime + + // How many working minutes were added according to the schedule + var mins = Math.round(sched.duration(start, due) / 60000); + + result.ok = true; + result.due = due; + result.minutesAdded = mins; + return result; + } catch (e) { + result.message = String(e); + return result; + } + }, + + /** + * Calculate working minutes between two times using the schedule. + * @returns {Object} { ok:Boolean, minutes:Number, message:String } + */ + workingMinutesBetween: function(scheduleSysId, startGdt, endGdt, timeZone) { + var out = { ok: false, minutes: 0, message: '' }; + try { + this._assertSchedule(scheduleSysId); + var start = this._toGdt(startGdt); + var end = this._toGdt(endGdt); + if (start.after(end)) { + out.message = 'start must be <= end'; + return out; + } + var sched = new GlideSchedule(scheduleSysId, timeZone || ''); + out.minutes = Math.round(sched.duration(start, end) / 60000); + out.ok = true; + return out; + } catch (e) { + out.message = String(e); + return out; + } + }, + + /** + * Find the next time that is inside the schedule window at or after fromGdt. + * @returns {Object} { ok:Boolean, nextOpen:GlideDateTime|null, message:String } + */ + nextOpen: function(scheduleSysId, fromGdt, timeZone) { + var out = { ok: false, nextOpen: null, message: '' }; + try { + this._assertSchedule(scheduleSysId); + var from = this._toGdt(fromGdt); + var sched = new GlideSchedule(scheduleSysId, timeZone || ''); + + // If already inside schedule, return the same timestamp + if (sched.isInSchedule(from)) { + out.ok = true; + out.nextOpen = new GlideDateTime(from); + return out; + } + + // Move forward minute by minute until we hit an in-schedule time, with a sane cap + var probe = new GlideDateTime(from); + var limitMinutes = 24 * 60 * 30; // cap search to 30 days + for (var i = 0; i < limitMinutes; i++) { + probe.addSecondsUTC(60); + if (sched.isInSchedule(probe)) { + out.ok = true; + out.nextOpen = new GlideDateTime(probe); + return out; + } + } + out.message = 'No open window found within 30 days'; + return out; + } catch (e) { + out.message = String(e); + return out; + } + }, + + /** + * Check if a time is inside the schedule. + * @returns {Object} { ok:Boolean, inSchedule:Boolean, message:String } + */ + isInSchedule: function(scheduleSysId, whenGdt, timeZone) { + var out = { ok: false, inSchedule: false, message: '' }; + try { + this._assertSchedule(scheduleSysId); + var when = this._toGdt(whenGdt); + var sched = new GlideSchedule(scheduleSysId, timeZone || ''); + out.inSchedule = sched.isInSchedule(when); + out.ok = true; + return out; + } catch (e) { + out.message = String(e); + return out; + } + }, + + // ---------- helpers ---------- + + _toGdt: function(val) { + if (val instanceof GlideDateTime) return new GlideDateTime(val); + if (typeof val === 'string' && val) return new GlideDateTime(val); + if (!val) return new GlideDateTime(); // default now + throw 'Unsupported datetime value'; + }, + + _assertSchedule: function(sysId) { + if (!sysId) throw 'scheduleSysId is required'; + var gr = new GlideRecord('cmn_schedule'); + if (!gr.get(sysId)) throw 'Schedule not found: ' + sysId; + }, + + type: 'BusinessTimeUtils' +}; diff --git a/Core ServiceNow APIs/GlideDateTime/Business time utilities (add, diff, next open, in schedule)/README.md b/Core ServiceNow APIs/GlideDateTime/Business time utilities (add, diff, next open, in schedule)/README.md new file mode 100644 index 0000000000..3642bf3d0f --- /dev/null +++ b/Core ServiceNow APIs/GlideDateTime/Business time utilities (add, diff, next open, in schedule)/README.md @@ -0,0 +1,31 @@ +# Business time utilities (add, diff, next open, in schedule) + +## What this solves +Teams repeatedly reimplement the same business-time maths. This utility wraps `GlideSchedule` with four practical helpers so you can: +- Add N working hours to a start date +- Calculate working minutes between two dates +- Find the next open time inside a schedule +- Check if a specific time is inside the schedule window + +All functions return simple objects that are easy to log, test, and consume in Flows or Rules. + +## Where to use +- Script Include in global or scoped app +- Call from Business Rules, Flow Actions, or Background Scripts + +## Functions +- `addWorkingHours(scheduleSysId, hoursToAdd, startGdt, tz)` +- `workingMinutesBetween(scheduleSysId, startGdt, endGdt, tz)` +- `nextOpen(scheduleSysId, fromGdt, tz)` +- `isInSchedule(scheduleSysId, whenGdt, tz)` + +## Notes +- The schedule determines both business hours and holidays. +- `tz` is optional; if omitted, the schedule’s TZ or instance default applies. +- All inputs accept either `GlideDateTime` or ISO strings (UTC-safe). + +## References +- GlideSchedule API + https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideSchedule/concept/c_GlideScheduleAPI.html +- GlideDateTime API + https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideDateTime/concept/c_GlideDateTimeAPI.html diff --git a/Core ServiceNow APIs/GlideDateTime/Business time utilities (add, diff, next open, in schedule)/example_background_usage.js b/Core ServiceNow APIs/GlideDateTime/Business time utilities (add, diff, next open, in schedule)/example_background_usage.js new file mode 100644 index 0000000000..82b8075838 --- /dev/null +++ b/Core ServiceNow APIs/GlideDateTime/Business time utilities (add, diff, next open, in schedule)/example_background_usage.js @@ -0,0 +1,21 @@ +// Background Script demo for BusinessTimeUtils +(function() { + var SCHEDULE_SYS_ID = 'PUT_YOUR_SCHEDULE_SYS_ID_HERE'; + var TZ = 'Europe/London'; + + var util = new BusinessTimeUtils(); + + var start = new GlideDateTime(); // now + var addRes = util.addWorkingHours(SCHEDULE_SYS_ID, 16, start, TZ); + gs.info('Add 16h ok=' + addRes.ok + ', due=' + (addRes.due ? addRes.due.getDisplayValue() : addRes.message)); + + var end = new GlideDateTime(addRes.due || start); + var diffRes = util.workingMinutesBetween(SCHEDULE_SYS_ID, start, end, TZ); + gs.info('Working minutes between start and due: ' + diffRes.minutes); + + var openRes = util.nextOpen(SCHEDULE_SYS_ID, new GlideDateTime(), TZ); + gs.info('Next open ok=' + openRes.ok + ', at=' + (openRes.nextOpen ? openRes.nextOpen.getDisplayValue() : openRes.message)); + + var inRes = util.isInSchedule(SCHEDULE_SYS_ID, new GlideDateTime(), TZ); + gs.info('Is now in schedule: ' + inRes.inSchedule); +})();