Sum cells if they are not strikethrough - javascript

I'm a little bit stuck with my Google Apps script purposed to calculate a sum of cells which are not strikethrough.
Here is its source:
function SumIfNotStrikethrough(rangeA1Notation)
{
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var mysheet = sheet.getActiveSheet();
var dataRange = mysheet.getDataRange();
var mydatarange = mysheet.getRange(rangeA1Notation);
var numRows = mydatarange.getLastRow();
var rowindex = mydatarange.getRowIndex();
var columnindex = mydatarange.getColumnIndex();
var total =0;
for(i=rowindex;i<=numRows;i++)
{
if(dataRange.offset(i-1, columnindex-1, 1, 1).isBlank() != true && dataRange.offset(i-1, columnindex-1, 1, 1).getFontLine() != "line-through")
{
var temp = dataRange.offset(i-1, columnindex-1, 1, 1).getValue();
total = total + temp;
}
}
return total;
}
Here is its formula: =SumIfNotStrikethrough("J2").
I have two questions here:
How to add the Google Apps script to use this formula as, for example, SumIfNotStrikethrough(J2) rather than SumIfNotStrikethrough("J2")? Quotes are so annoying in terms of changing its range manually after scaling the formula on other cells :-(. What should be changed in the source code?
When I run this script I face with following error:
Range not found (line 9, file "SumIfNotStrikethrough"
Thus, how can I fix it?
UPD № 1. Here is an example with a string reference, but it only counts the number of cells:
function countStrike(range) {
var count = 0;
SpreadsheetApp.getActiveSheet()
.getRange(range)
.getFontLines()
.reduce(function (a, b) {
return a.concat(b);
})
.forEach(function (el) {
if (el === "line-through") {
count++
}
});
return count;
}
UPD № 2. Unfortunately, this question is different from my previous question "Sum cells if they are not bold". I respectively tried to change the script, but it didn't work. Yes, it sums cells but it sums all the cells with strike-through and without :-(.
Here is what I changed:
function SumIfNotStrikethrough(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = SpreadsheetApp.getActiveSheet();
var formula = SpreadsheetApp.getActiveRange().getFormula();
var args = formula.match(/=\w+\((.*)\)/i)[1].split("!");
try {
if (args.length == 1) {
var range = sheet.getRange(args[0]);
}
else {
sheet = ss.getSheetByName(args[0].replace(/'/g, ''));
range = sheet.getRange(args[1]);
}
}
catch(e) {
throw new Error(args.join("!") + " is not a valid range");
}
var weights = range.getFontLine();
var numbers = range.getValues();
var x = 0;
for (var i = 0; i < numbers.length; i++) {
for (var j = 0; j < numbers[0].length; j++) {
if (weights[i][j] != "line-through" && typeof numbers[i][j] == "number") {
x += numbers[i][j];
}
}
}
return x;
}
Thank you for all your attempts to help me in advance!

I've modified the suggested answer with getFontLines() rather that getFontLine():
function SumIfNotStrikethrough1(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = SpreadsheetApp.getActiveSheet();
var formula = SpreadsheetApp.getActiveRange().getFormula();
var args = formula.match(/=\w+\((.*)\)/i)[1].split("!");
try {
if (args.length == 1) {
var range = sheet.getRange(args[0]);
}
else {
sheet = ss.getSheetByName(args[0].replace(/'/g, ''));
range = sheet.getRange(args[1]);
}
}
catch(e) {
throw new Error(args.join("!") + " is not a valid range");
}
var lines = range.getFontLines();
var numbers = range.getValues();
var x = 0;
for (var i = 0; i < numbers.length; i++) {
for (var j = 0; j < numbers[0].length; j++) {
if (lines[i][j] != "line-through" && typeof numbers[i][j] == "number") {
x += numbers[i][j];
}
}
}
return x;
}
Here is the formula to use it: =SumIfNotStrikethrough1(A2:B3).
Now that seems to be working properly in its single use.

Related

Randomize Unique Named Groups of Arrays with no Repetition of Elements per Grouping

I have 149 groups composed of individuals from a list of ~250. The grouping (tribe) composition is fixed, meaning the 3 members of each group cannot be switched. I need to create a schedule (6 days total) that will distribute the 149 groups into 25 groups per day, except the last day, with the condition that no single individual should have more than 1 occurrence per day.
Here is a copy of my dataset with dummy names.
Here is my Google Apps Script code so far:
var ui = SpreadsheetApp.getUi();
var sht = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Tribes');
var schedSht = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Schedule');
var nameSht = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Names');
var tribesArr = sht.getRange(2, 1, sht.getLastRow() - 1, 1).getValues();
var tribesOrder = tribesArr.map(function(t) { return t[0] });
tribesArr = tribesArr.map(function(t) { return t[0] });
var tribes = randomize(tribesArr);
var takenTribes = [];
var tribeSet = [];
var nameSet = [];
var LastDayTribeNum = 24;
var otherDaysTribeNum = 25;
var numDays = 6;
function makeMenu() {
ui.createMenu('My Menu').addItem('Create Randomized Schedule', 'makeSchedule').addToUi();
}
function makeSchedule() {
var dayNum = 1;
while (dayNum < numDays + 1) {
var remainingTribes = tribes.filter(function(r) { return takenTribes.indexOf(r) < 0; });
var daySched = fillUpDay(dayNum, remainingTribes);
tribeSet.push(daySched.tribes);
nameSet.push(daySched.names);
dayNum++;
}
remainingTribes = tribes.filter(function(r) { return takenTribes.indexOf(r) < 0; });
if (takenTribes.length < tribes.length) {
remainingTribes.forEach(swap);
}
tribeSet.forEach(function(tribe) {
schedSht.appendRow(tribe);
});
nameSet.forEach(function(name) {
nameSht.appendRow(name);
});
}
function fillUpDay(revDay, remTribes) {
var sched = {};
var namesToday = [];
var tribesToday = [];
var currIdx = 0;
var tribeMaxNum = (revDay === numDays) ? LastDayTribeNum : otherDaysTribeNum;
while (tribesToday.length < tribeMaxNum && currIdx < remTribes.length) {
var row = tribesOrder.indexOf(remTribes[currIdx]) + 2;
var currNames = sht.getRange(row, 2, 1, 3).getValues()[0];
var nameFound = false;
//check if name of tribe already in today's list
for (var t = 0; t < 3; t++) {
if (namesToday.indexOf(currNames[t]) !== -1) {
nameFound = true;
}
}
if (!nameFound) {
//if names of all three not in the list, add all three to today's list and remove tribe from list of unscheduled tribes
namesToday = namesToday.concat(currNames);
tribesToday.push(remTribes[currIdx]);
takenTribes.push(remTribes[currIdx]);
currIdx++;
} else {
//go to next tribe
currIdx++;
}
}
sched.tribes = tribesToday;
sched.names = namesToday;
return sched;
}
function swap(tribeToSwap) {
var row = tribesOrder.indexOf(tribeToSwap) + 2;
var namesInTribe = sht.getRange(row, 2, 1, 3).getValues()[0];
var len = nameSet.length;
var idx = 0;
var found = checkInSet(namesInTribe, nameSet[idx]);
var swapped = false;
while (!swapped && idx < len - 1) {
if (!found) {
// try swap
var tempNameArr = nameSet[idx];
tempNameArr.slice(3);
tempNameArr.push(namesInTribe);
var removedNames = nameSet[idx];
removedNames.slice(0, 3);
var lastNames = nameSet[len - 1];
var good = true;
for (var c = 0; c < 3; c++) {
if (lastNames.indexOf(removedNames[c]) > -1) {
good = false;
}
}
if (good) {
// swap
tribeSet[idx].slice(1);
nameSet[idx] = tempNameArr;
tribeSet[len - 1].push(tribeToSwap);
nameSet[len - 1].push(removedNames);
swapped = true;
}
} else {
// try next day
idx++;
found = checkInSet(namesInTribe, nameSet[idx]);
}
}
}
function checkInSet(names, setNames) {
var found = false;
for (var idx = 0; idx < names.length; idx++) {
if (setNames.indexOf(names[idx]) > -1) {
found = true;
}
}
return found;
}
function randomize(array) {
//https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
var currentIndex = array.length,
temporaryValue, randomIndex;
while (0 !== currentIndex) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
What I did was to create an array per day and check if a member of the current index's corresponding group (tribe) exists in the day's names array, and move on to the next index if found until the quota for the day (25) is reached. If names are not found for the day, the tribe will be marked as taken and will no longer be included in the subsequent schedules. Results will be appended to their respective sheets. If there are tribes that weren't matched because a name already appears for that day, I try to swap it in another day.
The code runs most of the time (gets completed in ~5s). Some other times, it runs for minutes that I cancel it. BUT, I never get what I need in any run. I will get 149 tribes in the Schedule sheet, but when I check the names, I don't get 447 names (with no repetition per day). I can't pinpoint where to adjust the algorithm. I hope someone can at least give hints, and better, a solution. Pardon the messy code.
I found out that the errors come from the swap() function. I revised the entire function this way:
function swap(tribeToSwap) {
var row = tribesOrder.indexOf(tribeToSwap) + 2;
var namesInTribe = sht.getRange(row, 2, 1, 3).getValues()[0];
var len = nameSet.length - 1;
var tempNames = [];
for (var days = 0; days < nameSet.length; days++) {
var currNameSet = nameSet[days];
tempNames[days] = currNameSet.concat(namesInTribe);
}
var noDupIdx = [];
tempNames.forEach(function (n) {
if (!checkDuplicate(n)) { noDupIdx.push(tempNames.indexOf(n)) };
});
var swapped = false;
for (var idx = 0; idx < noDupIdx.length; idx++) {
var currDay = noDupIdx[idx];
var allOrigNames = [nameSet[currDay], nameSet[len]];
var removedNames = allOrigNames[0].slice(0,3); //remove first 3 names
var newSoonNames = tempNames[currDay].slice(3); //add the names in swapping tribe
var newLateNames = allOrigNames[1].concat(removedNames);
if (!checkDuplicate(newLateNames)) {
nameSet[currDay] = newSoonNames;
nameSet[len] = newLateNames;
var swappedTribe = tribeSet[currDay][0];
tribeSet[currDay].slice(1).concat(tribeToSwap);
tribeSet[len] = tribeSet[len].concat(swappedTribe);
swapped = true;
break;
}
}
success.push(swapped);
}
I added a success array so that when the swap fails (as I've come to expect that is sometimes will), I will just get a UI alert about it.
I removed the checkInSet() function and replaced it with a checkDuplicate() function.
function checkDuplicate(arr) {
var map = {};
var duplicates = false;
for (var i = 0; i < arr.length; i++) {
if (map[arr[i]]) {
duplicates = true;
break;
}
map[arr[i]] = true;
}
return duplicates;
}
Good enough for now. Doesn't run forever anymore.

How to normalize an HTML table with rowspan and colspan into a matrix in JavaScript

Given a table such as this, how do you convert it to a table where every column and every row takes up one slot instead of many. I am trying but it's pretty difficult to wrap my head around.
var r = document.querySelectorAll('table tbody tr')
var w = []
var matrix = []
var rows = []
for (var i = 0, n = r.length; i < n; i++) {
rows.push([])
}
var c, b
for (var i = 0, n = r.length; i < n; i++) {
var x = r[i]
var d = x.querySelectorAll('td')
for (var j = 0, m = d.length; j < m; j++) {
var y = d[j]
while (b--) {
var column = y
row.push(column)
}
if (c > 0) {
rows[i].push()
c--
}
b = parseInt(y.getAttribute('colspan') || 1)
c = parseInt(y.getAttribute('rowspan') || 1)
}
}
function c1(el) {
}
I'm just considering the tbody part.
If I understood your question correctly, are you looking for something of this kind?
const myTable = document.querySelector('#my-Table tbody')
var matrix = []
for (let r=0; r<myTable.rows.length; r++ )
{
matrix[r] = []
for (let c=0;c<myTable.rows[r].cells.length; c++ )
{
matrix[r][c] = myTable.rows[r].cells[c].colSpan
}
}
Run this on that wikipedia page and tell me if that's what you're looking for. At least for columns.
const tds = [];
for (let td of document.querySelectorAll(`td, th`)) {
tds.push(td);
}
for (let td of tds) {
//debugger;
const colspan = td.getAttribute(`colspan`);
if (colspan) {
td.setAttribute(`colspan`, 1);
for (let i = 1; i <= colspan - 1; i++) {
td.after(document.createElement(`td`));
}
}
}
Edit: I see now that rows are much harder. (if this is what you want)
Which server side language are you using ?
Usually for these, you could just have a form which is posting data to another website's form. Look at this php example : https://www.ostraining.com/blog/coding/retrieve-html-form-data-with-php/
Correct me If I did not understand your question correctly.
This is what I was looking for.
print('.wikitable2')
function print(selector) {
var matrix = parse(selector)
var table = []
matrix.forEach(arr => {
arr = arr.map(x => x.replace(/,/g, ';'))
table.push(arr.join(','))
})
console.log(table.join('\n'))
}
function parse(selector) {
var rowEls = document.querySelectorAll(selector + ' thead th, ' + selector + ' tbody tr')
var matrix = []
for (var i = 0, n = rowEls.length; i < n; i++) {
var rowEl = rowEls[i]
var cellEls = rowEl.querySelectorAll('td, th')
var y = 0
for (var j = 0, m = cellEls.length; j < m; j++) {
var cellEl = cellEls[j]
var rowSpan = parseInt(cellEl.getAttribute('rowspan') || 1)
var cellSpan = parseInt(cellEl.getAttribute('colspan') || 1)
var val = parseCell(cellEl, j)
var rowSpanIterator = rowSpan
while (rowSpanIterator--) {
var cellSpanIterator = cellSpan
while (cellSpanIterator--) {
var x = i + rowSpanIterator
matrix[x] = matrix[x] || []
matrix[x][y + cellSpanIterator] = val
}
}
y += cellSpan
}
}
return matrix
}
function parseCell(el, i) {
return el.textContent.trim()
}

Google App script: Function to return an arrray of unique values between 2 two ranges

I am trying to write a custom function that will return an array of all of the unique values when two arrays are compared. This is what I have and it is not working:
function getUniqueCells(range1, range2) {
var sheet = SpreadsheetApp.getActiveSheet();
var range1 = sheet.getRange(range1);
var range2 = sheet.getRange(range2);
var range1Val = range1.getValues();
var range2Val = range2.getValues();
var uniquesArr = [];
for (var i = 0; i <= range1Val.length; i++) {
for(var u = 0; u <= range2Val.length; i++){
if(range1Val[i] === range2Val[u]) {
break;
} else if((u + 1) === range2Val.length){
uniquesArr.push(range1Val[i]);
};
};
};
return uniquesArr;
}
Is there a way not to get the "Internal error executing the custom function" error?
there is an issue on your second for loop.
for(var u = 0; u <= range2Val.length; i++){
it should be for(var u = 0; u <= range2Val.length; u++){
You cant check by range1Val[i] === range2Val[u]. Because they are arrays.
I had the same problem. found answer here:
compare rows on google spreadsheets

Check content of a "range" within a multidimensional array

I have a multidimensional array which is based on data from a spreadsheet. It is structured like this: Array[0][row][column].
Now I want to check if the values in several rows within one column are blank. Is there a way to concisely express something like if (Array[0][Row 1 to 10][Column 3] === "") ?
This is my current code:
function updateOverview() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var database = SpreadsheetApp.openById('XYZ');
var overviewContent = [ss.getDataRange().getValues()];
var campaignData = [database.getDataRange().getValues()];
for (var a = 1; a < (campaignData[0].length); a++) {
for (var d = 4; d < overviewContent[0].length; d++) {
if (campaignData[0][a][3] === overviewContent[0][0][1] && (Utilities.formatDate(campaignData[0][a][7],"GMT+01:00","yyyyMMdd")) <= (Utilities.formatDate(overviewContent[0][d][3],"GMT+01:00","yyyyMMdd")) && (Utilities.formatDate(campaignData[0][a][8],"GMT+01:00","yyyyMMdd")) >= (Utilities.formatDate(overviewContent[0][d][3],"GMT+01:00","yyyyMMdd"))) {
var campaignDuration = (Utilities.formatDate(campaignData[0][a][8],"GMT+01:00","yyyyMMdd") - Utilities.formatDate(campaignData[0][a][7],"GMT+01:00","yyyyMMdd"));
var campaignSlotAvailable = true;
for (var b = d; b < (campaignDuration +1); b++) {
if (overviewContent[0][b][5] !== "") {
campaignSlotAvailable = false;
break;
}
}
if (campaignSlotAvailable) {
overviewContent[0][d][5] === campaignData[0][a][6];
}
}
}
};
ss.getDataRange().setValues(overviewContent[0]);
};

Using Javascript find array values in another array and in order

Here is one to get your brain going! I've not had any luck with it.
[1,2,1,1,2,1,1,1,2,2]
[1,2,1,1,2,1]
I would like to use the second array to find the values in the first, but they must be in the same order.
Once for I would like it to return the next key up from the last key in the second array.
So in this example it would use the first six digits in the first array and then return 6 as the key after the final one in the second array.
var a2 = [1,2,1,1,2,1,1,1,2,2]
var a1 = [1,2,1,1,0,1]
function find(arr1, arr2) {
var len = 1
var result = 0;
var s2 = arr2.toString();
for (len=1;len <= a1.length; len++)
{
var aa1 = arr1.slice(0, len)
var s1 = aa1.toString();
if(s2.indexOf(s1)>=0){
result = aa1.length;
}
else {
break;
}
}
return result;
}
alert(find(a1, a2));
var find = function(haystack, needle) {
var doesMatch = function(offset) {
for (var i = 0; i < needle.length; i++) {
if (haystack[i+offset] !== needle[i]) {
return false;
}
}
return true;
};
for (var j=0; j < haystack.length - needle.length; j++) {
if (doesMatch(j)) {
return j;
}
}
return -1;
};
This is quick, this is dirty, and this is correct only if your data doesn't include any comma.
var needle = [1,2,1,1,2,1];
var haystack = [1,2,1,1,2,1,1,1,2,2];
if ( needle.length <= 0 ) return 0;
var fromStr = ','+haystack.toString()+','
var findStr = ','+needle.toString()+','
// Find ',1,2,1,1,2,1,' in ',1,2,1,1,2,1,1,1,2,2,'
var pos = fromStr.indexOf(findStr);
// Count the end position requested
return pos >= 0 ? fromStr.slice(0,pos+1).match(/,/g).length + needle.length - 1 : -1;
Note: The comma at head and tail is to make sure [22,12] doesn't match [2,1].

Categories

Resources