This task concerns Google Sheets and a Google Apps Scripts I wrote.
There is a task: we have a table which consists of 1 colomn and 3 rows. Each row (which is a cell also) contains a record of the following type "4d 1a 3c 5e 2b", where each letter can be subsituted by a word. In each row (cell) expressions may or may not repeat the "4d 1a 3c 5e 2b" expression, the distribution of numbers is random.
I need to do the following thing - divide each cell with "4d 1a 3c 5e 2b" type expression into 5 separate cells, sort them in ascending order.
On the one hand, I solved this problem. Firstly I created a variable and assigned the expression from the cell as a value to it. Then I created an array by means of .split(" ") method and sorted the items of the array by .sort() method. As a result I managed to set the values, which were items of sorted array, for a few cells.
Here is the code:
function mySort() {
var dt = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("myPage").getRange("F3").getValue();
var arr = dt.split(" ");
a = arr.sort();
var first = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("myPage").getRange("H3").setValue(arr[0]);
var second = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("myPage").getRange("I3").setValue(arr[1]);
var third =SpreadsheetApp.getActiveSpreadsheet().getSheetByName("myPage").getRange("J3").setValue(arr[2]);
var ddt = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("myPage").getRange("F4").getValue();
var arr2 = ddt.split(" ");
aa = arr2.sort();
var first2 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("myPage").getRange("H4").setValue(arr2[0]);
var second2 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("myPage").getRange("I4").setValue(arr2[1]);
var third2 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("myPage").getRange("J4").setValue(arr2[2]);
var dddt = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("myPage").getRange("F5").getValue();
var arr3 = dddt.split(" ");
aa = arr3.sort();
var first3 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("myPage").getRange("H5").setValue(arr3[0]);
var second3 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("myPage").getRange("I5").setValue(arr3[1]);
var third3 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("myPage").getRange("J5").setValue(arr3[2]);
}
As you can see, everything works: when I push the button with mySort() function assigned I get an appropriate result. However, I would like do more. It would be great if I could select as many rows as I want, then push the button and see how the loop goes through all the rows, splits them, sorts them, and assigns the elements of each sorted array to the cells next to the initial cell. To make it more clear, the final result must look something like this:
"4d 1a 3c 5e 2b" /// "1a" "2b" "3c" "4d" "5e"
"1g 5i 2g 4f 3h" /// "1g" "2g" "3h" "4f" "5i"
"5n 3m 1l 4k 2o" /// "1l" "2o" "3m" "4k" "5n"
So, how can I use loop in Google Sheets which will do such thing without typing in which exactly cell I want to see the items of the sorted array?
I think this will do it for you.
function yourSort(){
var ss=SpreadsheetApp.getActive();
var sh=ss.getSheetByName('myPage');
var rg=sh.getDataRange();
var vA=rg.getValues();
for(var i=2;i<vA.values();i++){
var A=String(vA[i][5]).split(" ").sort();
vA[i][7]=A[0];
vA[i][8]=A[1];
vA[i][9]=A[2];
}
rg.setValues(vA);
}
As long as you have headers for every column and some data on every row then I think getDataRange() will work for you.
Related
I have a Google form that collects responses in a linked spreadsheet. One of the fields in the form is Username (column B in the sheet). I'm writing a script to notify me when a new user (unique username) makes a submission. To do this, I:
Get all the values in column B except the latest one
Get the latest username
Check if the latest username is in the list of values (do something if not).
Here's what I've got so far:
function isUserNew() {
var spreadsheet = SpreadsheetApp.openById("INSERT ID HERE");
var sheet = SpreadsheetApp.setActiveSheet(spreadsheet.getSheets()[0]);
var last_row_number = sheet.getLastRow()
var penultimate_row_number = (last_row_number-1)
var latest_username = sheet.getRange(last_row_number, 2).getValue()
var usernames = sheet.getRange("B2:B" + penultimate_row_number).getValues();
}
This gives me a 2D array usernames that looks like [[UserA],[UserB],[UserC],[UserC],[UserA],[UserX]]. Then I tried if (usernames.indexOf(latest_username) == -1 but this doesn't work - it only works for a 1D array. I thought of converting the array to 1D using a loop:
for (var i = 0; i < usernames.length; i++) {
Logger.log(usernames[i][0]);
}
This makes the logger correctly log all the usernames I want, but I don't know how to put them into an array.
2 main questions:
How do I get the values into an array instead of the Logger?
Is this the best way to do what I'm trying to do? It seems unnecessary that every time there's a submission, I grab a 2D array, convert to 1D, and compare. The column data is not going to change, it only grows by 1 every time. How will this impact run time as the data grows into the 1000s?
Yep, you could flatten the array and compare with indexOf as you're doing:
var flattened_usernames = [] ;
for (var i = 0; i < usernames.length; i++) {
flattened_usernames.push(usernames[i][0]);
}
But you're better off just doing the check inside the loop:
if (usernames[i][0] === latest_username)
By the way, I assume you know how to grab the username directly from the onFormSubmit event rather than grabbing it from the last row of the sheet. But if not you should learn how to do that!
Instead of:
var usernames = sheet.getRange("B2:B" + penultimate_row_number).getValues();
Do:
var usernames = [].concat(...sheet.getRange("B2:B" + penultimate_row_number).getValues());
This flattens the 2d result of a column into a 1d array. If it's not the shortest way it should be close. I don't know about the efficiency.
I have an array in cell A1 via
A1 = {=G6:J6} = {"aa"."b"."ccc".1}
Now I want to use the cell A1 for array formula in B1. Basically B1 should be
B1 = SUMPRODUKT((C6:C12)*(B6:B12=G6:J6))
But instead of the direct reference to G6:J6 I would like to use A1 instead. I just tried:
B1 = SUMPRODUKT((C6:C12)*(B6:B12=A1))
B1 = {=SUMPRODUKT((C6:C12)*(B6:B12=A1))}
But this would not work. Is there a way to make it work?
Greetings,
Peter
For questions that appeared:
Cells G6:J6 are input data for example article numbers. I want to setup the input data only once in my sheet so I have to update less data. entries in G6:J6 are strings or numbers. Let's say G6 = "aa", H6 = "b", I6 = "ccc" and J6 = 1.
Cell B1 is one point where I need to use the data. It would rather be in another sheet but for simpler examples let's assume it is cell B1. In B1 I could of course refer to G6:J6 but this makes formular less easy to read. Therefore I would like to put a reference A1 next to B1 so one can see easily what data B1 uses.
C6:C12 is some numbers and B6:B12 is some strings/numbers that maybe match G6:J6. So sumproduct should sumup the matches.
Your cell A1 contains an array formula or array range but it only contains a single value from that array or range (each Excel cell can only contain a single result value).
So you need to replace the A1 in your SUMPRODUCT with an array or range expression.
Cell A1 value shall be G6:J6
G6:J6 populated as required with {"aa","bb","ccc",1} then in B1 put following formula and check if this is what you need.
=SUMPRODUCT(C6:C12*ISNUMBER(SEARCH(B6:B12,INDIRECT(A1))))
I have a web service response that provides me a block of data (in a long string), which I split into separate elements using the hard return as the separator. This gives me several sentences or elements (indexes I think), and each one has several data values within each element. For example:
//Gets data from web service response<br>
Def longstring =
"0 * 549 F7 G8 H9
1 2247 F6 G4 H10
17JUN DFWPHX F7
M7 B2 Y1"
//Splits the above into separate sentences/elements
longstring.split("\\r?\\n")
String[] Element=longstring.split("\\r?\\n")
//Print out of elements<br>
Log.info Element[1] = "0 * 549 F7 G8 H9"
Log.info Element[2] = "1 2247 F6 G4 H10"
Log.info Element [3] = "17JUN DFWPHX F7"
Log.info Element[4]= " M7 B2 Y1"
I have written a block of groovy code, which when provided the element ID, the code will try and drill down to get only a certain value within that element. For example, If Element[1], starts with "0" then do "x" thing, else do "y" thing. I need to be able to loop through all the elements (or indexes) with this same code until I come away with the information I need, then exit the iteration/loop once that data has been found.
I am not a groovy expert. I've seen the google results for maps, loops, and different operators. None of them make sense with my scenario. The text in each element is not a list. Mapping and looping seem to require a different set up than what I have. If you can help me solve this, please explain the code in simple terms if possible. Thanks in advance for your time and expertise.
It's a bit hard to understand your need here, but I will give it a try.
Assuming you want some piece of code to be executed based on the pattern of a line. I have an example here for you that tries do achieve this:
//Here I define 2 action closures - what I want to happen
def whenZero = { line ->
println "The line '$line' starts with a zero"
}
def whenOne = {line ->
println "The line '$line' starts with a one"
}
//Then I declare patterns and map them to each of the actions
Map methodMap = ['0.*':whenZero, '1.*':whenOne]
//This method will do the matching of the pattern and call any action
def executeBasedOnKey(Map map, String line){
map.each{ key, method ->
if (line.matches(key)) method(line)
}
}
//Your input
def longstring ="""0 * 549 F7 G8 H9
1 2247 F6 G4 H10
17JUN DFWPHX F7
M7 B2 Y1"""
//This line calls the evaluation for each of the lines
longstring.split("\\r?\\n").each{line->
executeBasedOnKey(methodMap, line)
}
This is the solution that worked for me. I labeled it based upon how I was told it was designed
//String that needs to be split
def longString ="""0 * 549 F7 G8 H9
1 2247 F6 G4 H10
17JUN DFWPHX F7
M7 B2 Y1"""
//Splits the entire string response above into substrings based upon hard returns ("S" becomes the new chopped up strings)
longString.split("\\r?\\n")
String[] S=longString.split("\\r?\\n")
//Creates the variable foundData for the loop to use below
String foundData;
//Creates "subS" variable for all the elements seen in the array "S" from above
for(String subS: S)
{
//Creates a matcher pattern to look in each subelement (subS) where it looks for a regex pattern.
//Description of the regex used: /\d\s+(\d+).*/ = one digit followed by one or more spaces, followed by another one or more digits
//Note that the regex above includes: (\d+). This creates a "group" within the pattern, which is referred to below in the DataMatcher
def DataMatcher = (subS =~ /\d\s+(\d+).*/);
//If the subelement (subS) matches the above pattern, it goes into the below block of code
if (DataMatcher.matches())
{ //Sets the variable foundData (see above) to value of the DataMatcher and only grabs the data needed
//Note that the flightMatcher has two sections [0] and [1]. These represent the following:
//[0] = The entire string should be looked at
//[1] = Only group 1 in that string should be retrieved *See group created (in comments) in the regex portion above
foundData = DataMatcher[0][1];
//This breaks the loop once the Data needed has been matched
break;
}
}
//Displays the foundData needed
log.info foundData
I have an array within an array and I am trying to name the variables using a for loop as there are a lot of variables. When I use the following simple code Time1 = dataCOMB{1,1}{1,1}(1:1024, 1); it opens the first cell in an array and proceeds to open the first cell in the following array and finally defines all the values in column 1 rows 1 to 1024 as Time1. However I have 38 of these different sets of data and when I apply the following code:
for t = 1:38
for aa = 1:38
Time(t) = dataCOMB{1,1}{1,aa}(1:1024, 1);
end
end
I get an error
In an assignment A(I) = B, the number of elements in B and I must be the same.
Error in Load_Files_working (line 39)
Time(t) = dataCOMB{1,1}{1,aa}(1:1024, 1);
Basically I am trying to get matlab to call the first column in each data set Time1, Time2, etc.
The problem:
1)You'd want to extract in a cell row...
2) ...the first 1024 numbers in the 1st column...
3) ...from each of the first 38 cells of a cell array.
The plan:
1) If one wants to get info from each element of a cell array (that is, an array accessed via {} indexing), one may use cellfun. Calling cellfun(some_function, a_cell_array) will aggregate the results of some_function(a_cell_array{k}) for all possible k subscripts. If the results are heterogeneous (i.e. not having the same type and size), one may use the cell_fun(..., 'UniformOutput', false) option to put them in an output cell array (cell arrays are good at grouping together heterogeneous data).
2) To extract the first 1024 numbers from the first column of an numeric array x one may use this anonymous function: #(x) x(1:1024,1). The x argument will com from each element of a cell array, and our anonymous function will play the role of some_function in the step above.
3) Now we need to specify a_cell_array, i.e. the cell array that contains the first 38 cells of the target. That would be, simply dataCOMB{1,1}(1,1:38).
The solution:
This one-liner implements the plan:
Time = cellfun(#(x) x(1:1024,1), dataCOMB{1,1}(1,1:38), 'UniformOutput', false);
Then you can access your data as in this example:
this_time = Time{3};
Your error is with Time(t). That's not how you create a new variable in matlab. To do exactly what you want (ie, create variables names Time1, Time2, etc...you'll need to use the eval function:
for aa = 1:38
eval(['Time' num2str(aa) '= dataCOMB{1,1}{1,aa}(1:1024,1);']);
end
Many people do not like recommending the eval function. Others wouldn't recommend moving all of your data out of a data structure and into their own independently-named variables. So, to address these two criticisms, a better alternative might be to pull your data out of your complicated data structure and to put it into a simpler array:
Time_Array = zeros(1024,38);
for aa = 1:38
Time_Array(:,aa) = dataCOMB{1,1}{1,aa}(1:1024,1);
end
Or, if you don't like that because you really like the names Time1, Time2, etc, you could create them as fields to a data structure:
Time_Data = [];
for aa = 1:38
fieldname = ['Time' num2str(aa)];
Time_Data.(fieldname) = dataCOMB{1,1}{1,aa}(1:1024,1);
end
And, in response to a comment below by the original post, this method can be extended to further unpack the data:
Time_Data = [];
count = 0;
for z = 1:2;
for aa = 1:38
count = count+1;
fieldname = ['Time' num2str(count)];
Time_Data.(fieldname) = dataCOMB{1,z}{1,aa}(1:1024,1);
end
end
I am making a script allows you to consolidate a todo list after finished items are erased.
I am getting a range, using an if statement to push all non-zero cell values from the specified range to an array, clearing the cell range and then re-pasting the new array (minus the cells with no data) back into the same range. Here is my code: (thanks mogsdad!)
function onOpen()
{
var spreadsheet = SpreadsheetApp.getActive();
var menuItems = [
{name: 'Consolidate To-Do list', functionName: 'consolidatetodolist_'}
];
spreadsheet.addMenu('LB functions', menuItems);
};
function consolidatetodolist_()
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var range = sheet.getRange("A2:A19");
var rangeValues = range.getValues();
var todosArray = new Array();
for (var i = 0 ; i < rangeValues.length ; i++ ) {
var row = rangeValues[i];
var cell = row [0];
if (null !== cell && "" !== cell) {
todosArray.push(row);
};
};
Logger.log(todosArray);
range.clearContent();
range.offset(0,0,todosArray.length).setValues(todosArray);
};
Your edit trigger should not be manipulating the menu. You should have an onOpen trigger do that. If you want the consolidation to be automatic, then you could drive THAT from your onEdit.
In your consolidatetodolist_() function, you've got a handful of errors regarding value assignment, Javascript fundamentals.
Using the SpreadsheetApp services to get the value of valueInstance isn't necessary. You've already got it, in rangeValues[i][0]. Note the two indexes; the data stored in rangeValues is a two-dimensional array, even though your range was one column. The first index is rows, the second is columns. (Opposite order from A1Notation.)
if ( range.getLength(valueInstance) !== 0 ) should crash, because range is an instance of Class Range which has no getLength() method. What you intend to check is whether the current cell is blank. Since spreadsheet data can be of three javascript types (String, Number, Date), you need a comparison that can cope with any of them, as well as an empty cell which is null. Try if (null !== cell && "" !== cell)...
When you get to setValues, you will need to supply a two-dimensional array that is the same dimensions as range. First, you need to ensure todosArray is an array of arrays (array of rows, where a row is an array of cells). Easiest way to do that here is to use var row = rangeValues[i]; var cell = row[0];, then later todosArray.push(row);. Since you are clearing the original range first, you could then define a new range relative to it using offset(0,0,todosArray.length).setValues(todosArray).
Other nitpicking:
You've got the idea of using meaningful variable names, good. But instead of valueInstance, cell would make this code clearer.
Use of "magic numbers" should be avoided, because it makes it harder to understand, maintain, and reuse code. Instead of i < 18, for example, you could use i < rangeValues .length. That way, if you modified the size of the range you're consolidating, the for loop would adapt without change.
Declaring var i outside of the for loop. No error with what you've done (although a declaration with no value assigned is bad), and it makes no difference to the machine, but for humans it's clearer to define i in the for loop, unless you need to use it outside of the loop. for (var i = 0; ...