I need a simple sequential unique ID.
The UNIQUE_ID column must be unique, and needs to be filled in for new rows if they are added. New rows can be added anywhere in the sheet, and I do not want the unique ID of any of the previously existing rows to be changed, so it cannot simply be based on the row index.
If you want to try Google Script then you can try below:
function onEdit(){
uniqueId();
}
function uniqueId() {
const ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('sheet1')//change sheet name per your requirement;
const lr = ss.getDataRange().getLastRow();
const idRange = ss.getRange(2,6,lr-1).getValues() //change range per your requirement;
const dataRange = ss.getRange(2,1,lr-1).getValues() //change range per your requirement;
for (i=0; i<dataRange.length; i++){
if(dataRange[i] !=''){
var maxId = Math.max.apply(null, ss.getRange(2,6,lr-1).getValues());
if(idRange[i] == '')
ss.getRange(i+2,6).setValue(maxId+1);
}
}
}
Remove the onEdit Function if you do want to run it on every edit
I constructed a mega-formula that applies various math to word lengths, ASCII codes at different positions and more:
=ArrayFormula(IF(A2:A="",,LEFT(TEXT(TRUNC((MMULT(IFERROR(CODE(SPLIT(LOWER(REGEXREPLACE(A2:A,"(.)","$1|"))&REPT("|",MAX(LEN(A2:A))-LEN(A2:A)),"|",1,0)),0),SEQUENCE(MAX(LEN(A2:A))+1,1,1,0))/CODE(MID(A2:A,1,1))*CODE(MID(A2:A&" ",2,1))-IFERROR(CODE(MID(A2:A,3,1))/(CODE(RIGHT(A2:A))-(CODE(LEFT(A2:A))/IFERROR(CODE(MID(A2:A,4,1)),13))-(CODE(LEFT(A2:A))/IFERROR(CODE(MID(A2:A,6,1)),17))),7)),10)*10^8,"0000000000"),8)))
Applying it against 5000 of the most common words in English plus your shortlist, I worked it out to where there were no duplicates (i.e., every word was assigned a unique code). NOTE: While the generated IDs are all unique, they are not sequential. If that is necessary, this approach will not work for your purposes (but I'm submitting it because it may address the needs of future site visitors).
As written, the word list is in A2:A. The formula would go in the Row-2 cell of the column where you want the codes.
See if that works for you.
SIDE NOTE: I find myself curious as to why one would need a unique code if the words themselves are all unique.
Related
I have little to no coding knowledge, so apologies if the solution is too obvious!
I am trying to add a Last Modified column to a Google Sheets file. To do this, I am using an AppScript function with the following code:
function setTimestamp(x) {
if(x != ""){
return new Date();
}
}
This works fine when I use setTimestamp(x) in my file. However, I am combining this with a Zapier action that creates a new row whenever new media is added. Every time a new row is created, any existing formulas are removed.
I assume I need to use ARRAYFORMULA to apply the setTimestamp formula to newly-created rows, but it must only apply to rows that aren't blank.
I have tried the following:
={"Last Modified";ARRAYFORMULA(setTimestamp(A2:A))} -> Only worked on first row
={"Last Modified";ARRAYFORMULA(B2:B=setTimestamp(A2:A))} -> Broke the file
={"Last Modified";ARRAYFORMULA(IF(A2:A)=1,setTimestamp(A2:A),"")} -> Expected 1 argument, got 3
Is there a way I can combine the IF into the script or a better way to solve the problem?
A public version of my file is available here: https://docs.google.com/spreadsheets/d/13zkVRPr2Wh5bHjCT8cenInHnBk7qkMkuEMdwUxC_cRU/edit?usp=sharing
All data is dummy data and stock photos.
Unfortunately, arrayformula does not function as an array map function for custom functions. (Even for native functions where you may expect it to work that way, it does not always, sadly.)
To handle array range, we need the custom function to handle array range directly. That also limits the number of individual calls to custom functions, which materially saves execution time.
To handle array range, there are 2 ways. I'll comment on both.
Array range directly as input of custom function
If the input is a single cell, it is read directly
If the input range spans more than a single cell, the data is read as nested lists: a list of lists of rows.
For example, A1 will be read as the data in A1. A1:B2 will be read as [[A1, B1], [A2, B2]].
You can remember it as columns of rows.
As for the input data format, numbers are taken without the display format. Texts are taken as strings.
If output is an array range, the result will automatically expend.
Thus, in your example, in B2 you can almost do
=setTimestamp(A2:A)
where setTimestamp() has been modified to
function out = setTimestamp(arr) {
out=Array(mat.length);
for (i=0;i<mat.length;i++){
j=0
if(arr[i][j] != ""){
out[i]=new Date();
}
}
return out
}
For more details, see the official help page. (Over the years, more details have become available.)
Almost, but not quite. For your direct question, above provides the answer. However, you seem to have an implicit requirement that your custom function is executed every time a new URL is found. Be careful that what happens here is that every time Google Sheet updates cell content, a new Date() is created and outputted.
Array range read within custom function
Since you know your URLs are in A2:A, and you want the output of your custom function to be B2:B, you can read and modify those ranges directly within your custom function via the Range Class.
In this route, you may find getLastRow(), getLastColumn() in Sheet and getNextDataCell() in Range convenient.
When you need to execute your custom function, you can run it manually or add onEdit() trigger to your custom function. (But onEdit() itself can mean substantial UI lag when using the sheet. It's usually more appropriate for sheets that parse external data automatically. See other triggers in the link for motions.)
In your example, you can almost do
function setTimestamp() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var lastRow = sheet.getLastRow();
var row=1;
var cell = sheet.getRange(row,1).getValue();
while (row<=lastRow) {
if(cell.getValue() != ""){
sheet.getRange(row,2).setValue(new Date());
}
cell = sheet.getRange(row,1,lastRow).getNextDataCell(SpreadsheetApp.Direction.DOWN);
row=cell.getRow();
}
}
which will scan for all URLs in A2:A and write current time to B2:B when executed.
Again, your example implicitly points to updating only when a new URL is found. So be careful about that. Use triggers as needed.
As for the need to place formula in B1, you can (and should) reference the output of your other application in a different sheet so that you or a different application of yours can edit without conflict.
Thus, for what was asked, we have everything.
I was wondering if there was a way to find duplicate values in Google sheets regardless of formatting errors and also fix them.
For example, list one is literally the same as list two. But in sheets the duplicates aint picked up.
List One:
Alcatel
Apple
Benq-Siemens
Blackberry
Google
HTC
Huawei
LG
Manufacturer
Motorola
Nokia
One Plus
Samsung
Sony-Ericcson
List Two:
Manufacturer
Alcatel
apple
benqsiemens
Blackberry
Google
hTC
Huawei
lg
Manufacturer
Motorola
Nokia
One Plus
Samsung
Sonyericcson
Please note in the List Two the only ones with errors as in formatting errors are apple,benqsiemens,hTC,lg,Sonyericsson.
How do I do it so that the two list have all duplicates selected despite any formatting errors and also fix them?
Thanks
Use this formula
=ArrayFormula(IFERROR(IF(REGEXMATCH(PROPER(C2:C),REGEXEXTRACT(PROPER(B2:B),"\w+")),B2:B,"MANUAL FIX")))
MANUAL FIX you have when formula can not fix it.
You have to first fix C and then formula will work.
I think you can check out example Example 4. Compare two Google Sheets for differences to compare the two sheets https://www.ablebits.com/office-addins-blog/2019/04/30/google-sheets-compare-two-sheets-columns/
Here is a sample code on how to fix the format of your list two based on list one:
function fixDuplicates() {
var listOne = 'Sheet2!A2:A';
var listTwo = 'Sheet2!B2:B';
var sheet = SpreadsheetApp.getActiveSheet();
var listOneVal = sheet.getRange(listOne).getDisplayValues().flat().filter(String);
var listTwoRange = sheet.getRange(listTwo);
var listTwoVal= listTwoRange.getDisplayValues().flat().filter(String);
//clone list two value
var tmpListTwo = [...listTwoVal];
//Update Temp List Two Values,remove spaces, special characters and set to uppercase
tmpListTwo.forEach((val,index) => {
tmpListTwo[index] = val.replace(/[^a-zA-Z0-9]/g,"").toUpperCase();
});
//Find list one value in temp list two
listOneVal.forEach(val =>{
//Remove spaces, special characters and set to uppercase
var tmp = val.replace(/[^a-zA-Z0-9]/g,"").toUpperCase();
var index = tmpListTwo.indexOf(tmp);
if(index != -1){
Logger.log("Finding: "+listTwoVal[index]);
var textFinder = listTwoRange.createTextFinder(listTwoVal[index]);
var firstOccurrence = textFinder.findNext();
if(firstOccurrence!=null){
//duplicate found, fix the duplicate format. set cell value based on list one value
firstOccurrence.setValue(val);
Logger.log("Value set to: "+val);
}
}
});
}
What it does?
Define your list one and two range in A1Notation (Including the sheet name)
Get the values of your list one and two, change 2-d array to 1-d array using array.flat() and remove empty-cell values using array.filter()
Clone your list two array. Format your temporary list two array by removing all special characters, spaces and set it to upper case.
Loop all your list one value, transform your current list one value by also removing special characters, spaces and set to upper case. Get the index of list one value in your temporary list two array.
Once we determined the duplicates in list two, we will use Range.createTextFinder(findText) to search for the duplicate's range. Then set its value using Range.setValue(value) using your list one value.
Output:
See your original list two value in column F
After running the code, it was transformed to the one in column B
See Cell C1, on how to get the duplicates in column B using the formula: =Filter(B2:B,MATCH(A2:A,B2:B,0))
I want a formula to generate random data of birth dates for a specific years (Example: 1995 to 2002) and make it Array like this:
Sheet URL: https://docs.google.com/spreadsheets/d/1XHoxD-hNmpUOMVm_u-cz-4ESrabodsrS0fIfaN-n4js/edit
That might not be the best approach but it will get you closer to what you want:
=DATE(RANDBETWEEN(1995,2002),RANDBETWEEN(1,12),RANDBETWEEN(1,31))
There are two issues with this approach:
you might get a day that does not exist for the particular month. For example, 2/28/2021 exists, but 2/29/2021 does not exist.
I wasn't able to generate an array but only drag down formulas. When I generate an array, the same random numbers are used and as a result the dates are the same.
For the first issue, you can use isdate to check if the random date returned is correct. For example, 2/29/2021 is a wrong date (I hardcopied that date).
but I guess you can filter out the FALSE cases.
I really hope other people can come up with a better approach.
You could try (as I demonstrated in your sheet):
=ARRAY_CONSTRAIN(SORT(SEQUENCE(DATE(1992,12,31)-DATE(1900,1,1),1,DATE(1900,1,1)),RANDARRAY(DATE(1992,12,31)-DATE(1900,1,1)),1),COUNTA(A2:A),1)
SEQUENCE(DATE(1992,12,31)-DATE(1900,1,1),1,DATE(1900,1,1)) - Is used to create an array of valid numeric representations of true dates between 1-1-1900 and 31-12-1992.
SORT(<TheAbove>,RANDARRAY(DATE(1992,12,31)-DATE(1900,1,1)),1) - Is used to sort the array we just created randomly.
ARRAY_CONSTRAIN(<TheAbove>, COUNTA(A2:A),1) - Is used to only return as many random birth-dates we need according to other data.
Note that this is volatile and will recalculate upon sheet-changes and such. Also note that this is just "slicing" a given array and may fall short when you try to use it on a dataset larger than the given array.
As Google Sheets can deal with dates as integers (~ number of days since 1900), choosing a random date between two dates can be a single call to RANDBETWEEN (with the output formatted as Date).
With your initial date written in B1 and your end date in B2, the formula is simply:
=RANDBETWEEN($B$1,$B$2)
You can paste this formula in as many cells as you want, to generate N different random dates.
Of course, as other answers involving random generators in your sheet, the formula will be recomputed at each change. My suggestion to overcome this would simply be to copy/paste the output, using the "Paste special > Values only" option (right click or "Edit" menu).
Script Solution
Just for sake of completeness, here is a solution using a script
Initial Considerations
This cannot function like a in sheet function/formula.
https://developers.google.com/apps-script/guides/sheets/functions
Custom function arguments must be deterministic. That is, built-in spreadsheet functions that return a different result each time they calculate — such as NOW() or RAND() — are not allowed as arguments to a custom function. If a custom function tries to return a value based on one of these volatile built-in functions, it will display Loading... indefinitely.
A custom function cannot affect cells other than those it returns a value to. In other words, a custom function cannot edit arbitrary cells, only the cells it is called from and their adjacent cells. To edit arbitrary cells, use a custom menu to run a function instead.
So a normal script is needed.
The Script
/**
* Sets the values of a range to random dates.
*/
function generateRandomBdays(range, start, end) {
let height = range.getHeight();
let width = range.getWidth();
let output = [];
for (let i = 0; i != height; i++) {
let row = [];
for (let j = 0; j != width; j++) {
row.push(randomBday(start, end));
}
output.push(row)
}
range.setValues(output);
}
/**
* Generates a random date beween start and end
*/
function randomBday(start, end) {
if (start < 2000) start = start - 1900
start = new Date(`${start}`);
if (end < 2000) end = end - 1900
end = new Date(`${end}`);
let bday = new Date(
start.getTime() + (Math.random() * (end.getTime() - start.getTime()))
);
return bday;
}
/**
* Gets active selection and fills with random dates
*/
function main(){
let file = SpreadsheetApp.getActive();
let sheet = file.getActiveSheet()
let range = sheet.getActiveRange();
// ============
generateRandomBdays(range, 1995, 2002); // Change these years to your liking
// ============
}
/**
* Creates menu when sheet is opened.
*/
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Generate Birthdays')
.addItem('Generate!', 'main')
.addToUi();
}
Which works like this:
Installation
You will have to copy it into your script editor and then run one of the functions to authorize the script with the permissions it needs. Then next time you open the sheet you should have the menu available.
Alternatively you can delete the onOpen function and just use it from the script editor.
Within the main function, customize the range of years you need.
References
Apps Script
Overview of Spreadsheet Service in Apps Script
I'm working with google forms and google sheets. I'm trying to create a summary sheet that will automatically update as the form is being filled.
I've been able to pull the data from the other sheets using a FILTER function. Now I want to add a column that shows the name of a country to the filtered column. I tried using concatenate but it didn't work as well as I'd hoped. Can someone help me figure out how to solve this problem.
Please see here for an example of the problem.
Well this is a very inelegant brute force way, but I think it works. See Solution-GK in your sheet.
=QUERY({
{TRANSPOSE(SPLIT(REPT("Nigeria~",ROWS(UNIQUE(FILTER(NIGERIA!A:E,NIGERIA!C:C<TODAY(),NIGERIA!B:B="Charity Fundraiser")))),"~")),
UNIQUE(FILTER(NIGERIA!A:E,NIGERIA!C:C<TODAY(),NIGERIA!B:B="Charity Fundraiser"))};
{TRANSPOSE(SPLIT(REPT("Sierra Leone~",ROWS(UNIQUE(FILTER('SIERRA LEONE'!A:E,'SIERRA LEONE'!C:C<TODAY(),'SIERRA LEONE'!B:B="Charity Fundraiser")))),"~")),
UNIQUE(FILTER('SIERRA LEONE'!A:E,'SIERRA LEONE'!C:C<TODAY(),'SIERRA LEONE'!B:B="Charity Fundraiser"))}},
"select Col1,Col2,Col3,Col4, Col5 where Col2 is not null")
I've added a hard coded literal of the country name, repeated it the number of times needed for the matching data rows, and made it into the first column in your existing data array. I repeat this for the second array you have for the second country.
I'm sure there are far more elegant ways to do this, so we'll see what else is proposed. If you had a list somewhere in your sheet of your country names - ie. Nigeria and Sierra Leone, possibly many more - I'm sure an elegant solution would cycle through those names, pulling the name to build the concatenated data ranges, and also adding the name as the text for each row.
Without needing a list in the sheet, a little bit of code could find all of your tab names, and exclude the non data ones, eg. Solution Here and Summary, and process all of the rest as data.
Note: I'm not clear that you need your UNIQUE statements, unless you are expecting duplicates for some reason. Also, the outer QUERY doesn't seem to be necessary - the inner FILTERs seem to do everything you need.
You could do this with an Apps Script Custom Function.
First, open a bound script by selecting Tools > Script editor, and copy the following functions to the script (check inline comments for more details about the code):
// Copyright 2020 Google LLC.
// SPDX-License-Identifier: Apache-2.0
function SUMMARIZE_FUNDRAISING_EVENTS(sheetNames, ...ranges) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
sheetNames = sheetNames.split(","); // Comma-separated string to array of sheet names
const filteredData = sheetNames.map(sheetName => { // Iterate through each sheet name
const sheet = ss.getSheetByName(sheetName);
if (sheet) { // Check if sheet exists with this name
const sheetData = sheet.getRange(2,1,sheet.getLastRow()-1,4).getValues(); // Source sheet data
const filteredData = sheetData.filter(rowData => {
return rowData[1] === "Charity Fundraiser" && rowData[2] < new Date()
}); // Filter data according to date and type of event
filteredData.forEach(filteredRow => filteredRow.unshift(sheetName)); // Add sheet name to filtered data
return filteredData;
}
}).flat();
return filteredData;
}
Once it is defined, you can use the function SUMMARIZE_FUNDRAISING_EVENTS the same you would any sheets built-in function. This function would accept a series of parameters:
A comma-separated string with the names of the sheets whose data should be summarized (don't add blank spaces after the comma or similar).
The different source ranges (in your case, NIGERIA!A:E and 'SIERRA LEONE'!A:E).
Both of these are necessary, because, on the one side, specifying the source ranges as parameters makes sure that the function executes and updates the summarized data every time the source ranges are edited, and on the other side, when passed as parameters, these source ranges don't contain information about the sheet names, which the script will need when returning the summarized data.
Example of calling the function:
Reference:
Custom Functions in Google Sheets
I have the following formula
=average(arrayformula(indirect(split(A1,","))))
Where A1 contains a list of cell addresses, such as E4,E6,E12. I expect this to be equivalent to =AVERAGE(E4,E6,E12), but this does not behave as expected, yielding 4 no matter what the data in the cells are. Preliminary research indicates that the INDIRECT() function doesn't pass through ARRAYFORMULA() correctly. Attempting SUM() on the outside yields precisely the same results.
Any ideas on how to average the values of cells obtained indirectly by a list of cell addresses?
I do have a list of columns and the row doesn't ever change for this average calculation, so I'm wondering if I could do some kind of subset instead, such as
=AVERAGE(RANGE){LIST_TO_SUBSET_BY}
I'm not sure about a built-in formula to do this so I've written a custom function to do it for you.
Go to Tools -> Script editor and replace the existing function with the code below and then save the project.
Now in your spreadsheet in any cell =CUSTOMFUNCTION(A1) where A1 contains a list of comma-separated cell references.
NOTE:
Updating values in the referenced cells won't force a recalculation of this formula, only updating cell A1 will.
I suggest you also go to File -> Spreadsheet settings -> Calculation and change 'Recalculation' to 'On change and every minute' that will force a recalculation of this function every minute.
/**
* Returns the average value of a dataset.
* #param {"A1"} cell The cell containing the list of cell references.
* #return The input repeated a specified nunmber of times.
* #customfunction
*/
function CUSTOMAVERAGE(cell){
var ss = SpreadsheetApp.getActiveSheet();
var array = [];
var cellRefs = cell.split(",");
for(var i in cellRefs){
array.push(ss.getRange(cellRefs[i]).getValue());
}
var sum = 0;
for(var i in array){
sum += array[i]
}
var avg = sum/array.length;
return avg;
}
Though this is a very specific application in response to this question, for the sake of the knowledge base, I'd like to show how this can be done without a script.
To give this context, imagine the LIST_CELL is a list of question numbers
(which are entered in as a header row, call the range QUESTIONS) on a test that correspond to certain standards, and the goal is to average only the questions that correspond to the standard next to which the list is written, and for each student. Using
=iferror(join(",",ArrayFormula(match(split(LIST_CELL,","),QUESTIONS,FALSE))),"")
The split function splits the a hand-entered list of questions on commas, the match function returns the column number of that particular question in QUESTIONS, and the join function joins the data back together. ArrayFormula allows the match to be performed on an array instead of just the first value.
Another single row heading lists the standards to which each question has been matched (possibly to more than one standard) by the comma separated list in LIST_CELL. For a column list of students in A:A, each standard needs to average the scores of every question that is listed next to the standard. This is accomplished by the nifty (if clunky):
average(ArrayFormula(hlookup(split(vlookup(LOOKUP_VAL,SEARCH_RANGE,COL_W_LIST),","),DATA_SOURCE,row(CURRENT_CELL))))
Breakdown from center outward:
LOOKUP_VAL is the value being looked up (the one that has multiple matches); in the example context, it's the standard.
SEARCH_RANGE is a range of cells containing both the list of lookup value (the standards in context) and the comma separated lists of column numbers generated by the first function. COL_W_LIST is the column number in the array SEARCH_RANGE that contains the list of row numbers matched from LIST_CELL.
Split takes the elements apart and placed them in a temporary array so that hlookup can be performed on each element. Via ArrayFormula the hlookup grabs each value on the same row in the appropriate QUESTIONS column - in context, it grabs the point scores for each question matched to the standard.
Finally, average is self-explanatory, and does take an array as input apparently.
These two functions in combination allow of use of indirect cell references in an array formula, and solves the much asked, "how do I include multiple matches in a calculation" question. At least in this specific context.
EDIT
There is an example "template" with this implemented here. You'll need to make your own copy to edit it.