Skip to content

Instantly share code, notes, and snippets.

@deviationist
Last active May 2, 2023 07:21
Show Gist options
  • Save deviationist/05a4261f3eed7b9a559c97dd5a860a11 to your computer and use it in GitHub Desktop.
Save deviationist/05a4261f3eed7b9a559c97dd5a860a11 to your computer and use it in GitHub Desktop.
A set of functions for Google Sheets to sum columns by text formatting. Currently supports font weight, line-through, underline.
// These functions let's you sum columns based on their text formatting.
// The only flaw is that there's no text formatting change event, so you have to update the value of a field to trigger recalculation.
// styleRangeString (string) - The range where the text formatting should be evaluated. Example: A10:A20
// sumRangeString (string) - The range where the numerical values will be summed. Should correspond with styleRangeString unless you only specify column. Example: B10:B20
// attribute (string) - The text formatting attribute for conditional check during sum.
// Note: styleRangeString and sumRangeString must be passed as a STRING, meaning it has to be wrapped in quotes ("A10:A20").
// Example:
// =sumIfFontStyle("A10:A20", "B", "italic")
// =sumIfFontWeight("A10:A20", "B", "bold")
// These will give the same result
// =sumIfLineStyle("A10:A20", "B", "line-through")
// =sumIfNotLineStyle("A10:A20", "B10:B20", "line-through")
// These will give the same result
// =sumIfLineStyle("A10:A20", "A", "line-through")
// =sumIfNotLineStyle("A10:A20", "A10:A20", "line-through")
function sumIfLineStyle(styleRangeString, sumRangeString, lineStyle) {
return sumBase(styleRangeString, sumRangeString, (styleRange) => {
return styleRange.getFontLines(); // https://developers.google.com/apps-script/reference/spreadsheet/range#getfontlines
}, lineStyle, true);
}
function sumIfNotLineStyle(styleRangeString, sumRangeString, lineStyle) {
return sumBase(styleRangeString, sumRangeString, (styleRange) => {
return styleRange.getFontLines(); // https://developers.google.com/apps-script/reference/spreadsheet/range#getfontlines
}, lineStyle, false);
}
function sumIfFontStyle(styleRangeString, sumRangeString, style) {
return sumBase(styleRangeString, sumRangeString, (styleRange) => {
return styleRange.getFontStyles(); // https://developers.google.com/apps-script/reference/spreadsheet/range#getfontlines
}, style, true);
}
function sumIfNotFontStyle(styleRangeString, sumRangeString, style) {
return sumBase(styleRangeString, sumRangeString, (styleRange) => {
return styleRange.getFontStyles(); // https://developers.google.com/apps-script/reference/spreadsheet/range#getfontlines
}, style, false);
}
function sumIfFontWeight(styleRangeString, sumRangeString, fontWeight) {
return sumBase(styleRangeString, sumRangeString, (styleRange) => {
return styleRange.getFontWeights(); // https://developers.google.com/apps-script/reference/spreadsheet/range#getfontlines
}, fontWeight, true);
}
function sumIfNotFontWeight(styleRangeString, sumRangeString, fontWeight) {
return sumBase(styleRangeString, sumRangeString, (styleRange) => {
return styleRange.getFontWeights(); // https://developers.google.com/apps-script/reference/spreadsheet/range#getfontlines
}, fontWeight, false);
}
function sumBase(styleRangeString, sumRangeString, getAttributes, attributeComparison, booleanComparison) {
const { styleRangeObject, sumRangeObject } = handleRanges(styleRangeString, sumRangeString);
const ss = SpreadsheetApp.getActiveSpreadsheet();
const styleRange = ss.getRange(styleRangeObject.join(':'));
const sumRange = ss.getRange(sumRangeObject.join(':')).getValues();
const attributes = getAttributes(styleRange);
let x = 0;
for(var i = 0; i < attributes.length; i++) {
if(
(booleanComparison === true && attributes[i].toString() == attributeComparison)
|| booleanComparison === false && attributes[i].toString() != attributeComparison
) {
try {
const value = Number(sumRange[i].toString());
if (!isNaN(value)){
x += value;
}
} catch(e) {}
}
}
return x;
}
function onEdit(e) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const ranges = ss.createTextFinder('(sumIfLineStyle|sumIfNotLineStyle|sumIfFontStyle|sumIfNotFontStyle|sumIfFontWeight|sumIfNotFontWeight)')
.useRegularExpression(true)
.matchFormulaText(true)
.findAll();
for(var i = 0; i < ranges.length; i++) {
const formula = ranges[i].getFormula();
const pattern = new RegExp(/(sumIfLineStyle|sumIfNotLineStyle|sumIfFontStyle|sumIfNotFontStyle|sumIfFontWeight|sumIfNotFontWeight)\("(?<styleRangeString>[A-Z0-9]+|[A-Z0-9]+:[A-Z0-9]+)";\s*"(?<sumRangeString>[A-Z0-9]+|[A-Z0-9]+:[A-Z0-9]+)";\s*"(.+)"\s*(|[^)]+)\)/);
const matches = pattern.exec(formula);
if (!matches || !matches?.groups) return;
let { styleRangeString, sumRangeString } = matches.groups;
const { styleRangeObject, sumRangeObject } = handleRanges(styleRangeString, sumRangeString);
const styleRange = ss.getRange(styleRangeObject.join(':'));
const sumRange = ss.getRange(sumRangeObject.join(':'));
if(rangeIntersects(e.range, styleRange) || rangeIntersects(e.range, sumRange)) {
ranges[i].setFormula('');
SpreadsheetApp.flush();
ranges[i].setFormula(formula);
}
}
}
function getLetter(string) {
return string.replace(/[0-9]/g, ''); // Remove all numbers
}
function getNumber(string) {
return string.replace(/\D/g,''); // Remove all but numbers
}
function isOnlyLetter(string) {
return /^[A-Z]+$/.test(string);
}
function isLetterAndNumber(string) {
return /^[A-Z0-9]+$/.test(string);
}
function handleRanges(styleRangeRaw, sumRangeRaw) {
let styleRangeStart, styleRangeEnd, sumRangeStart, sumRangeEnd;
const styleRangeArray = styleRangeRaw.split(':').map(e => e.trim().toUpperCase());
const sumRangeArray = sumRangeRaw.split(':').map(e => e.trim().toUpperCase());
// Set style range
if (styleRangeArray.length === 1) {
styleRangeStart = styleRangeArray[0];
styleRangeEnd = styleRangeArray[0];
} else {
styleRangeStart = styleRangeArray[0];
styleRangeEnd = styleRangeArray[1];
}
// Set sum range start
sumRangeStart = sumRangeArray[0];
if (isOnlyLetter(sumRangeStart)) {
sumRangeStart = sumRangeStart + getNumber(styleRangeStart);
}
// Set sum range end
if (sumRangeArray.length === 1) {
if (isOnlyLetter(sumRangeArray[0])) {
sumRangeEnd = getLetter(sumRangeStart) + getNumber(styleRangeEnd);
} else {
sumRangeEnd = sumRangeStart;
}
} else {
sumRangeEnd = sumRangeArray[1];
if (isOnlyLetter(sumRangeEnd)) {
sumRangeEnd = sumRangeEnd + getNumber(styleRangeEnd);
}
}
if (
getLetter(styleRangeStart) != getLetter(styleRangeEnd)
|| getLetter(sumRangeStart) != getLetter(sumRangeEnd)
) {
throw new Error('Range must have the same letter (no multi-column support).');
}
return {
styleRangeObject: [styleRangeStart, styleRangeEnd],
sumRangeObject: [sumRangeStart, sumRangeEnd]
};
}
function rangeIntersects(R1, R2) {
const LR1 = R1.getLastRow();
const Ro2 = R2.getRow();
if (LR1 < Ro2) return false;
const LR2 = R2.getLastRow();
const Ro1 = R1.getRow();
if (LR2 < Ro1) return false;
const LC1 = R1.getLastColumn();
const C2 = R2.getColumn();
if (LC1 < C2) return false;
const LC2 = R2.getLastColumn();
const C1 = R1.getColumn();
if (LC2 < C1) return false;
return true;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment