diff --git a/Core ServiceNow APIs/GlideDateTime/Next Business Window Calculator/README.md b/Core ServiceNow APIs/GlideDateTime/Next Business Window Calculator/README.md new file mode 100644 index 0000000000..9b44a3e00c --- /dev/null +++ b/Core ServiceNow APIs/GlideDateTime/Next Business Window Calculator/README.md @@ -0,0 +1,36 @@ +# Next Business Window Calculator + +## Problem +You often need to compute the *next business window* for task scheduling, SLAs, or batch processing: “Start at 16:45, add 95 working minutes using schedule X and holiday set Y, and return both the end time and the ‘windows’ you traversed.” Native APIs give you the building blocks, but teams frequently re-invent this logic. + +## Where to use it +- Script Include utility callable from Business Rules, Flow/Schedule jobs, or Background Scripts +- Works in *scoped* apps + +## What it does +- Accepts: start `GlideDateTime`, working minutes (integer), a schedule sys_id (or name), and an optional timezone +- Uses `GlideSchedule` to hop across working/non-working periods and holidays +- Returns: + - `endGdt` — the calculated ending `GlideDateTime` + - `segments` — an ordered list of working sub-segments used (start/end per segment), useful for audit/debugging + - `consumedMinutes` — total minutes consumed + +## Configuration +At the top of the script you can configure: +- Default schedule sys_id (fallback) +- Default timezone (e.g., `Europe/London`) +- Maximum safety iterations + +## How it works +The utility constructs a `GlideSchedule` from the provided schedule id, aligns the starting point, then iteratively consumes the requested working minutes across schedule segments (respecting holidays and timezone). It avoids recursion and uses a safety counter to prevent infinite loops. + +## References +- GlideSchedule (Scoped) API. ServiceNow Docs. + https://www.servicenow.com/docs/ (GlideSchedule Scoped) +- Server API overview (Zurich docs bundle). :contentReference[oaicite:1]{index=1} + +## Example +```js +var util = new x_snc_example.NextBusinessWindow(); +var result = util.addWorkingMinutes('2025-10-21 16:45:00', 95, 'your_schedule_sys_id', 'Europe/London'); +// result.endGdt.getDisplayValue() -> "2025-10-22 09:20:00" diff --git a/Core ServiceNow APIs/GlideDateTime/Next Business Window Calculator/next_business_window.js b/Core ServiceNow APIs/GlideDateTime/Next Business Window Calculator/next_business_window.js new file mode 100644 index 0000000000..07ca93b1b4 --- /dev/null +++ b/Core ServiceNow APIs/GlideDateTime/Next Business Window Calculator/next_business_window.js @@ -0,0 +1,81 @@ +var NextBusinessWindow = Class.create(); +NextBusinessWindow.prototype = (function () { + // Configuration + var DEFAULT_SCHEDULE_SYS_ID = ''; // optional fallback schedule sys_id (cmn_schedule) + var DEFAULT_TZ = 'Europe/London'; + var MAX_SEGMENTS = 1000; // safety guard + + // Helpers + function _toGdt(input) { + if (input instanceof GlideDateTime) return input; + var g = new GlideDateTime(); + if (input) g.setDisplayValue(input); + return g; + } + function _getSchedule(scheduleSysId) { + var sch = new GlideSchedule(); + if (scheduleSysId) sch.load(scheduleSysId); + else if (DEFAULT_SCHEDULE_SYS_ID) sch.load(DEFAULT_SCHEDULE_SYS_ID); + else throw new Error('No schedule sys_id supplied and no default configured.'); + return sch; + } + function _setTz(gdt, tz) { if (tz) gdt.setTZ(tz); return gdt; } + + return { + initialize: function () {}, + + /** + * Add working minutes across a GlideSchedule. + * @param {String|GlideDateTime} start + * @param {Number} minutes + * @param {String} scheduleSysId + * @param {String} [timeZone] IANA name, e.g. "Europe/London" + * @returns {{endGdt: GlideDateTime, consumedMinutes: number, segments: Array}} + */ + addWorkingMinutes: function (start, minutes, scheduleSysId, timeZone) { + if (!minutes || minutes < 0) throw new Error('Minutes must be a positive integer.'); + var tz = timeZone || DEFAULT_TZ; + var sch = _getSchedule(scheduleSysId); + + var cursor = _toGdt(start); + _setTz(cursor, tz); + + // If not in schedule, jump to the next working start + if (!sch.isInSchedule(cursor)) { + var nextStart = sch.getNextStartTime(cursor); + if (!nextStart) throw new Error('No next working period found from start time.'); + cursor = nextStart; + } + + var remaining = parseInt(minutes, 10); + var segments = []; + var guard = 0; + + while (remaining > 0) { + if (guard++ > MAX_SEGMENTS) throw new Error('Exceeded max segments; check schedule/inputs.'); + + var segEnd = sch.getNextEndTime(cursor); + if (!segEnd) throw new Error('Schedule has no next end time; check configuration.'); + + var available = Math.ceil((segEnd.getNumericValue() - cursor.getNumericValue()) / (60 * 1000)); // minutes + + if (remaining <= available) { + var end = new GlideDateTime(cursor); + end.addSeconds(remaining * 60); + segments.push({ segmentStart: new GlideDateTime(cursor), segmentEnd: new GlideDateTime(end), consumed: remaining }); + return { endGdt: end, consumedMinutes: minutes, segments: segments }; + } + + // consume full segment + segments.push({ segmentStart: new GlideDateTime(cursor), segmentEnd: new GlideDateTime(segEnd), consumed: available }); + remaining -= available; + + var nextStart = sch.getNextStartTime(segEnd); + if (!nextStart) throw new Error('No subsequent working segment found.'); + cursor = nextStart; + } + + return { endGdt: new GlideDateTime(cursor), consumedMinutes: minutes, segments: segments }; + } + }; +})();