Skip to content

Instantly share code, notes, and snippets.

@emmaly
Last active December 1, 2023 13:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save emmaly/2522d2e12157c4c3ebefe32d409ca996 to your computer and use it in GitHub Desktop.
Save emmaly/2522d2e12157c4c3ebefe32d409ca996 to your computer and use it in GitHub Desktop.
Google Sheet: doRangesIntersect() = do all of these ranges have shared overlap points? getRangeIntersection() = return the intersection of *all* of any number of supplied ranges
/**
* Source: https://gist.github.com/emmaly/2522d2e12157c4c3ebefe32d409ca996
*
* @param {SpreadsheetApp.Range} ranges
* @returns {boolean}
* - `true` indicates all ranges share the same overlapping area with all others.
* - `false` indicates no shared overlap area, even if they all overlap individually.
*/
function doRangesIntersect(...ranges) {
return getRangeIntersection(...ranges) !== null;
}
function example_DoesIntersect() {
const sheet = SpreadsheetApp.getActiveSheet();
const ranges = [
sheet.getRange(5, 5, 4, 4),
sheet.getRange(6, 6, 4, 4),
sheet.getRange(7, 7, 4, 4),
];
// ranges.forEach((range) => Logger.log("range: %s", range.getA1Notation()));
const hasIntersection = doRangesIntersect(...ranges);
Logger.log("hasIntersection: %s", hasIntersection);
const intersection = getRangeIntersection(...ranges);
Logger.log("intersection: %s", intersection?.getA1Notation());
}
// sharedOverlap: true
// union: G7:H8
function example_DoesNotIntersect() {
const sheet = SpreadsheetApp.getActiveSheet();
const ranges = [
sheet.getRange(4, 4, 4, 4),
sheet.getRange(5, 5, 4, 4),
sheet.getRange(6, 6, 4, 4),
sheet.getRange(7, 7, 4, 4),
sheet.getRange(8, 8, 4, 4),
];
// ranges.forEach((range) => Logger.log("range: %s", range.getA1Notation()));
const hasIntersection = doRangesIntersect(...ranges);
Logger.log("hasIntersection: %s", hasIntersection);
const intersection = getRangeIntersection(...ranges);
Logger.log("intersection: %s", intersection?.getA1Notation());
}
// sharedOverlap: false
// union: null
/**
* @param {SpreadsheetApp.Sheet} sheet
* @param {boolean?} obeyFrozenRows - default `true`
* @param {boolean?} obeyFrozenCols - default `false`
* @returns {SpreadsheetApp.Range}
*/
function getDataRangeFromSheet(sheet, obeyFrozenRows=true, obeyFrozenCols=false) {
const firstRow = 1 + (!!obeyFrozenRows ? sheet.getFrozenRows() : 0);
const firstCol = 1 + (!!obeyFrozenCols ? sheet.getFrozenColumns() : 0);
const numRows = sheet.getMaxRows() - (firstRow-1);
const numCols = sheet.getMaxColumns() - (firstCol-1);
return sheet.getRange(firstRow, firstCol, numRows, numCols);
}
/**
* Calculates the intersection of multiple given ranges within a Google Sheet.
*
* The function determines the single range that encompasses the overlapping area of the
* provided ranges. It attempts to return as early as possible when a failure condition is
* met, such as in cases where an intersection is impossible or input is invalid, or where an
* intersection can be quickly resolved.
*
* In cases where ranges do not overlap or where any ranges are on a different sheet, then an
* intersection is not found, and therefore will return `null`.
*
* Returns a single `Range` encompassing the overlapping area common to all provided ranges.
*
* Source: Emmaly - https://gist.github.com/emmaly/2522d2e12157c4c3ebefe32d409ca996
*
* @param {SpreadsheetApp.Range[]} ranges - An array of `Range` objects from Google Sheets.
*
* The ranges should be from the same sheet.
* The ranges must be supplied as arguments, not as
* `Range[]` but as `...ranges` (using the spread
* operator) or `range1, range2, range3` (as
* individual `Range` arguments).
*
* To aid in debugging, this will throw a helpful
* error if an array of `Range`s is mistakenly passed
* in as the first argument, rather than giving false
* negative results.
*
* If *any* arguments are not a `Range`, this will
* throw an error indicating the argument number
* at fault. Take care to never pass in a non-`Range`,
* value as an argument. But if you might, then you
* will need to catch the exception when it occurs.
* @returns {SpreadsheetApp.Range} - A single `Range` object representing the intersection of
* all provided ranges.
*
* Returns `null` if there is no intersection across *all*
* provided ranges or if any ranges are from different sheets.
*/
function getRangeIntersection(...ranges) {
if (!ranges || !Array.isArray(ranges) || ranges.length < 1) {
throw new Error("expected `...ranges`");
}
if (ranges.length === 1 && !!ranges[0].getA1Notation) {
// only one range passed in? this is already resolved.
return ranges[0];
}
if (Array.isArray(ranges[0])) {
// if first argument is an array...
// then it's likely programmer error.
throw new Error("expected `...ranges` not `ranges[]`");
}
for (let i=0; i<ranges.length; i++) {
if (typeof ranges[i].getA1Notation !== "function") {
throw new Error("expected `Range` at argument #" + (i+1).toFixed(0));
}
Logger.log("[%s] %s", i.toFixed(0), ranges[i].getA1Notation());
}
const intersectionOf = (rangeA, rangeB) => {
// missing ranges can't have an intersection
if (!rangeA || !rangeB) return null;
// sheet containing the potential intersection
const sheet = rangeA.getSheet();
// no intersection if ranges are from different sheets
if (sheet.getSheetId() !== rangeB.getSheet().getSheetId()) return null;
// find our intersection box
const left = Math.max(rangeA.getColumn(), rangeB.getColumn());
const right = Math.min(rangeA.getLastColumn(), rangeB.getLastColumn());
const top = Math.max(rangeA.getRow(), rangeB.getRow());
const bottom = Math.min(rangeA.getLastRow(), rangeB.getLastRow());
// test for failed intersection
if (left > right || top > bottom) return null;
// return the intersection's range
return sheet.getRange(
top,
left,
bottom - top + 1,
right - left + 1
);
};
return ranges.slice(1).reduce((acc, curr) => acc && intersectionOf(acc, curr), ranges[0]);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment