Get range of columns in empty sheet - google-sheets

Consider the google sheet is empty and the amount of columns are unknown:
How do I find out the range of this EMPTY sheet?
I tried:
$this->service->spreadsheets_values->get($sheetId, $sheetName)
But it just returns an empty range. Only when I add any data will it show me the range, but I need to know this in advance before I add data.

While I will be refactoring this, I just wanted to say that I did figure this out with the following code:
$raw = $this->service->spreadsheets_values->get($sheetId, $sheetName)->getRange();
$split = explode('!', $raw);
$range = explode(':', $split[1]);
$letters = [];
$letter = preg_replace('/[0-9]+/', '', $range[0]);
while ($letter) {
$letters[] = $letter++;
if ($letter == preg_replace('/[0-9]+/', '', $range[1])){
break;
}
if (sizeof($letters) > 1000)
{
break;
}
}
return $letters;

Related

Highlight near duplicate in conditional formating to highlight values with one character difference

I'm currently using this formula to highlight duplicates in my spreadsheet.
=ARRAYFORMULA(COUNTIF(A$2:$A2,$A2)>1)
Quite simple, it allows me to skip the first occurrence and only highlight 2nd, 3rd, ... occurrences.
I would like the formula to go a bit further and highlight near duplicates as well.
Meaning if there is only one character difference between 2 cells, then it should be considered as a duplicate.
For instance: "Marketing", "Marketng", "Marketingg" and "Market ing" would all be considered the same.
I've made a sample sheet in case my requirement is not straightforward to understand.
Thanks in advance.
Answer
Unfortunately, it is not possible to do this only through Formulas. Apps Scripts are need as well. The process for achieving your desired results is described below.
In Google Sheets, go to Extensions > Apps Script, paste the following code1 and save.
function TypoFinder(range, word) { // created by https://stackoverflow.com/users/19361936
if (!Array.isArray(range) || word == "") {
return false;
}
distances = range.map(row => row.map(cell => Levenshtein(cell, word))) // Iterate over range and check Levenshtein distance.
var accumulator = 0;
for (var i = 0; i < distances.length; i++) {
if (distances[i] < 2) {
accumulator++
} // Keep track of how many times there's a Levenshtein distance of 0 or 1.
}
return accumulator > 1;
}
function Levenshtein(a, b) { // created by https://stackoverflow.com/users/4269081
if (a.length == 0) return b.length;
if (b.length == 0) return a.length;
// swap to save some memory O(min(a,b)) instead of O(a)
if (a.length > b.length) {
var tmp = a;
a = b;
b = tmp;
}
var row = [];
// init the row
for (var i = 0; i <= a.length; i++) {
row[i] = i;
}
// fill in the rest
for (var i = 0; i < b.length; i++) {
var prev = i;
for (var j = 0; j < a.length; j++) {
var val;
if (b.charAt(i) == a.charAt(j)) {
val = row[j]; // match
} else {
val = Math.min(row[j] + 1, // substitution
prev + 1, // insertion
row[j + 1] + 1); // deletion
}
row[j] = prev;
prev = val;
}
row[a.length] = prev;
}
return row[a.length];
}
In cell B1, enter =TypoFinder($A$2:$A2,$A2). Autofill that formula down the column by draggin.
Create a conditional formatting rule for column A. Using Format Rules > Custom Formula, enter =B2:B.
At this point, you might wish to hide column B. To do so, right click on the column and press Hide Column.
The above explanation assumes the column you wish to highlight is Column A and the helper column is column B. Adjust appropriately.
Note that I have assumed you do not wish to highlight repeated blank columns as duplicate. If I am incorrect, remove || word == "" from line 2 of the provided snippet.
Explanation
The concept you have described is called Levenshtein Distance, which is a measure of how close together two strings are. There is no built-in way for Google Sheets to process this, so the Levenshtein() portion of the snippet above implements a custom function to do so instead. Then the TypoFinder() function is built on top of it, providing a method for evaluating a range of data against a specified "correct" word (looking for typos anywhere in the range).
Next, a helper column is used because Sheets has difficulties parsing custom formulas as part of a conditional formatting rule. Finally, the rule itself is implemented to check the helper column's determination of whether the row should be highlighted or not. Altogether, this highlights near-duplicate results in a specified column.
1 Adapted from duality's answer to a related question.

Selecting chapters, section and subsection with google sheets with drop down list

we have currently a large number of exercises (for math) that need to be categorized according to an existing table of contents. Each exercise has a unique number. This number should be placed into the table of contents. The depth of the toc is 3, so we have chapters, sections and subsections.
I want three drop down lists for each of the (many) exercises. The first one selects the chapter, the second the section, and the third the subsection. I can solve this with two filtered lists that depend on certain filtering of a list seperated into chapter, section, subsection. That works fine for few exercises. But, i have to make the filtered lists for each exercise seperately. That is exactly the problem. I do not want to maintain 500 lists (two per exercise)
Is there any way to hardcode this? I do not want to scroll through the many subsections each time and I need a error control. So, one should not be able to select chapter 3 and then subsection 3 from chapter 4.
EDIT: Link to Google Sheet:
https://docs.google.com/spreadsheets/d/e/2PACX-1vSsRqFlLIkpgIrw18GBDLdUEl0FFmF5hSXIe2oAXztz9N50VNiO0eCP3cAB20KOgHU4nfH6gBFCWmyT/pubhtml
1. Separate the data and the dropdows
That's a best practice, because you will need to have some auxiliary cells to create the dependency
2. Create a simple data validation dropdown
Do it for the first column in a separate sheet
3. Create the auxiliary cells
Filter the second column with the values you have in the data validation to show the values adjacent to the first column
=FILTER(B2:B,A2:A=Dropdowns!$A$2)
4. Repeat the process with as many columns you have
Here is the sample spreadsheet for you to have an idea how to implement it
https://docs.google.com/spreadsheets/d/1VPsx1hfKuZDifMgrREOvUb4xEIvbVODnKPuRDWZUOF8/edit?usp=sharing
Update
I'm working on a script, I created two functions, at the moment they're not achieving the step you want, but I'm thinking on looping the number of columns as the same way I'm doing it with the rows.
const menuSheet = 'Values';
const dataSheet = 'Data';
const wsValues = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(menuSheet);
const wsData = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(dataSheet);
const data = wsData.getRange(2, 1, wsData.getLastRow() - 1, 3).getValues();
let firstCol = 1;
let secondCol = 2;
let thirdCol = 3;
/*
function setDropdownDependenciesTest() {
const list = ['a', 'b']
const cell = wsValues.getRange('C2')
setDropdownDependencies(list, cell)
}
*/
function onEdit(e) {
const activeCell = e.range
let val = activeCell.getValue()
let row = activeCell.getRow()
let column = activeCell.getColumn()
let wsName = activeCell.getSheet().getName()
if (wsName === menuSheet && column === firstCol && row > 1) {
applyFirstValidation(val, row)
} else if (wsName == menuSheet && column === secondCol && row > 1) {
applySecondValidation(val, row)
}
}
function applyFirstValidation(val, row) {
if (val === "") {
wsValues.getRange(row, secondCol).clearContent()
wsValues.getRange(row, secondCol).clearDataValidations()
wsValues.getRange(row, thirdCol).clearContent()
wsValues.getRange(row, thirdCol).clearDataValidations()
} else {
wsValues.getRange(row, secondCol).clearContent()
wsValues.getRange(row, secondCol).clearDataValidations()
wsValues.getRange(row, thirdCol).clearContent()
wsValues.getRange(row, thirdCol).clearDataValidations()
let filteredData = data.filter(info => {
return info[0] === val;
})
let listToApply = filteredData.map(info => {
return info[1]
})
let cell = wsValues.getRange(row, secondCol)
setDropdownDependencies(listToApply, cell)
}
}
function applySecondValidation(val, row) {
if (val === "") {
wsValues.getRange(row, thirdCol).clearContent()
wsValues.getRange(row, thirdCol).clearDataValidations()
} else {
wsValues.getRange(row, thirdCol).clearContent()
let firstColValue = wsValues.getRange(row, firstCol).getValue()
let filteredData = data.filter(info => {
return info[0] === firstColValue && info[1] === val;
})
let listToApply = filteredData.map(info => {
return info[2]
})
let cell = wsValues.getRange(row, thirdCol)
setDropdownDependencies(listToApply, cell)
}
}
function setDropdownDependencies(list, cell) {
const rule = SpreadsheetApp
.newDataValidation()
.requireValueInList(list)
.setAllowInvalid(false)
.build()
cell.setDataValidation(rule)
}

Why does GSheet's "find" not find my blank cells even though ISBLANK() finds them?

I have the following problem: When I type "^\s*$" into GSheet's "find" it does not find my blank cells even though ISBLANK() finds them. I need to find and replace the blank cells with "NA". Help would be greatly appreciated!
This is an excerpt of my table: https://docs.google.com/spreadsheets/d/12EajCPW68UXc8kgeqfsEoxgTBuLbccdAnnj6tZOSntM/edit#gid=0
try it like this:
=REGEXMATCH(C5, "^$")
^$
You can try this Apps Script code. This should get you started.
function replaceBlank() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var range = sheet.getRange(2,2, sheet.getLastRow(), 2);
var rangeval = range.getValues();
for (var i = 0; i < sheet.getLastRow()-1; i++) {
Logger.log(rangeval[i]);
if (rangeval[i].concat() == ",") {
rangeval[i].splice(0,2,"NA","NA");
sheet.getRange(i+2,2,1,2).setValues([rangeval[i]]);
} else {
}
}
}
The way this code works is that it will iterate through columns B and C and once it detects that the current row is blank, it will set the value NA as defined on rangeval[i].splice(0,2,"NA","NA");
Screenshot:

Setting a column to equal the negative of a row in Google Sheets

The Google Sheets API seems vague and I'm probably just too tired.
function onEdit(e) {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var positives = sheet.getRange("D3:AG3");
var negatives = sheet.getRange("C4:C33");
for (i=0;i<positives.getLastColumn();i++) {
var j = positives[i]*-1;
negatives[i].setValue(j);
}
}
I'm sure I'm doing eight things wrong but if someone is more familiar with Google Sheets, please throw a brick at me.
First, positives is a ranges, and you need to use getValues() to get an array that you can manipulate.
Second, it's not recommended to use Sheets API methods inside loops, the best practice is to manipulate arrays in loops and then use single get and set values API to read / write to a range.
Sample Code:
function onEdit(e) {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var positives = sheet.getRange("D3:AG3").getValues();
var negatives = sheet.getRange("C4:C33");
var result = [];
for (i = 0; i < positives[0].length; i++) {
result.push([positives[0][i] * -1]);
}
negatives.setValues(result);
}
Sample Output: (I only put values in three rows)
Reference:
push()
Avoid using onEdit for these kind of changes as it will be resource intensive. You are changing all the values of the column into negative of the row EVERY TIME you edit the sheet (Unless that should be the case)
If you really want to use onEdit, be sure to limit it only when the specific range is edited.
Code:
function onEdit(e) {
const row = e.range.getRow();
const column = e.range.getColumn();
// if edited range is within D3:AG3
if(row == 3 && column >= 4 && column <= 33) {
// write to the corresponding row (invert col and row)
e.source.getActiveSheet().getRange(column, row).setValue(e.value * -1);
}
}
Note:
Behaviour of the onEdit function is that when you edit the range D3:AG3, it will negate its value and write into its corresponding destination, one by one.
If you edit D3, it will assign that negative value into C4, nothing more.
If you edit outside the positive range, it will not do anything.
Another approach is to copy your positive row into negative column by transforming your data structure into the destination by bulk.
Code:
function rowToColumn() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var pRange = sheet.getRange("D3:AG3");
var pValues = pRange.getValues();
// pValues is a 2D array now
// row range values = [[1, 2, 3, ...]
var negatives = sheet.getRange("C4:C33");
// column range values = [[1], [2], [3], ...]
// since structure of row is different than column
// one thing we can do is convert the row into column structure
// and multiply each element with -1, then assign to negatives
pValues = pValues.map(function(item) {
item = item.map(function(col) {
return [col * -1];
});
return item;
})[0];
// set values into the negatives range
negatives.setValues(pValues);
}
Note:
Behaviour of the rowToColumn function is that it transfers all the values of the row range and then put it into negatives range all at once.
Blank cells will yield 0 by default, add a condition on return [col * -1]; if you want blank cells to return other values instead.
Output:

How to compare two column in a spreadsheet

I have 30 columns and 1000 rows, I would like to compare column1 with another column. IF the value dont match then I would like to colour it red. Below is a small dataset in my spreadsheet:
A B C D E F ...
1 name sName email
2
3
.
n
Because I have a large dataset and I want to storing my columns in a array, the first row is heading. This is what I have done, however when testing I get empty result, can someone correct me what I am doing wrong?
var index = [];
var sheet = SpreadsheetApp.getActiveSheet();
function col(){
var data = sheet.getDataRange().getValues();
for (var i = 1; i <= data.length; i++) {
te = index[i] = data[1];
Logger.log(columnIndex[i])
if (data[3] != data[7]){
// column_id.setFontColor('red'); <--- I can set the background like this
}
}
}
From the code you can see I am scanning whole spreadsheet data[1] get the heading and in if loop (data[3] != data[7]) compare two columns. I do have to work on my colour variable but that can be done once I get the data that I need.
Try to check this tutorial if it can help you with your problem. This tutorial use a Google AppsScript to compare the two columns. If differences are found, the script should point these out. If no differences are found at all, the script should put out the text "[id]". Just customize this code for your own function.
Here is the code used to achieve this kind of comparison
function stringComparison(s1, s2) {
// lets test both variables are the same object type if not throw an error
if (Object.prototype.toString.call(s1) !== Object.prototype.toString.call(s2)){
throw("Both values need to be an array of cells or individual cells")
}
// if we are looking at two arrays of cells make sure the sizes match and only one column wide
if( Object.prototype.toString.call(s1) === '[object Array]' ) {
if (s1.length != s2.length || s1[0].length > 1 || s2[0].length > 1){
throw("Arrays of cells need to be same size and 1 column wide");
}
// since we are working with an array intialise the return
var out = [];
for (r in s1){ // loop over the rows and find differences using diff sub function
out.push([diff(s1[r][0], s2[r][0])]);
}
return out; // return response
} else { // we are working with two cells so return diff
return diff(s1, s2)
}
}
function diff (s1, s2){
var out = "[ ";
var notid = false;
// loop to match each character
for (var n = 0; n < s1.length; n++){
if (s1.charAt(n) == s2.charAt(n)){
out += "–";
} else {
out += s2.charAt(n);
notid = true;
}
out += " ";
}
out += " ]"
return (notid) ? out : "[ id. ]"; // if notid(entical) return output or [id.]
}
For more information, just check the tutorial link above and this SO question on how to compare two Spreadsheets.

Resources