Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -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 };
}
};
})();
Loading