Last active
May 2, 2023 07:21
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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