Sort Range By Column Automatically - sorting

What I want to do: sort a range of data by the active column via shortcut key(s). I do this regularly and want to automate. However, the size and location of the range changes each time I do this.
My approach
Select a cell in the range of interest that's in the first row and specific column I want to sort by, call it C4
Select down to last row of the range
Select left and/or right to all columns of the range
Get column number / address of C4 (which should be 3, since C = column 3)
Sort range by column of C4
I've been able to accomplish 1. to 3. I'm struggling with 4. Below is what I've got. I suspect that if I can get 4. sorted, then then last line will accomplish 5.
function sortCol() {
// KGF - 22,49
// Purpose: automatically select a range of data and sort by column of active cell
// Status - wip; haven't determined how to get col # of active cell
// Last updated: 22-12-4
var spreadsheet = SpreadsheetApp.getActive();
var currentCell = spreadsheet.getCurrentCell();
let cc1 = currentCell;
spreadsheet.getSelection().getNextDataRange(SpreadsheetApp.Direction.DOWN).activate();
spreadsheet.getActiveRange().getDataRegion(SpreadsheetApp.Dimension.COLUMNS).activate();
// how to get col # of active cell?? 22,49
let cc1_col = cc1.getColumn
console.log(cc1_col)
Logger.log(cc1_col)
//
cc1.activateAsCurrentCell();
spreadsheet.getActiveRange().sort({column: cc1_col, ascending: true});
};

Related

Convert Google Sheets "sort" formula with embedded VLOOKUP into equivalent app script [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 14 days ago.
Improve this question
I have a Google Sheets "sort" formula that I want to convert into an app script equivalent so that I can apply the sorting to a set of rows whenever I click a button on the page. The difficult part is that the sort formula has an embedded VLOOKUP that is used to convert a string column into a number. Here is the formula:
=sort(D2:J,I2:I,TRUE,D2:D,TRUE,VLOOKUP(E2:E,'Frequency'!A:B,2,FALSE),TRUE,H2:H,TRUE,G2:G,TRUE)
The sort creates the output perfectly, but it does not affect the source rows. I would like the script to reorder the source rows exactly as the sort formula does. How can I accomplish this?
Name
Task
Type
Frequency
Room
Joe
Task 1
Routine
Weekly
203a
Jane
Task 2
Security
Daily
102
I have tried the usual Google searches for relevant results. The closest I could come was this:
`spreadsheet.getActiveRange()
.sort([
{column: 6, ascending: true},
{column: 1, ascending: true},
{column: 2, ascending: true},
{column: 5, ascending: true},
{column: 3, ascending: true}]);`
It sorts the columns, but uses only the exact values in the columns. I need my sort script to reference a lookup value first.
The VLOOKUP formula converts the text of "frequency" to a number with a key-value pair.
Frequency
Days
Daily
0
Weekly
7
Biweekly
14
Monthly
30
The source rows have the word in the cell, but we need to sort by the numeric value rather than alphabetically. The data rows are all text. The sort formula uses column I, then D, then the numeric value from the VLOOPUP, then H, then finally column G. The VLOOKP sorts to daily, weekly, biweekly, then monthly instead of biweekly, daily, monthly, then weekly alphabetically. All other columns are just sorted alphabetically.
How would the formula be done using apps script?
Thanks for being awesome!
You have a SORT problem. Your data includes a "Frequency" text field which does not sort reliably. So you have a table that matches each text values with a number of "Days" value.
Data on TabB is filtered and copied from raw data on TabA. You want to sort both TabA and TabB using the same sort logic.
TabB can be sorted with an internal SORT function because the formula can be written to include VLOOKUP (finding the "days" value for the respective "Frequency")
BUT Tab A cannot include the "Days" value, so sorting TabA data must be by script.
The following script takes the TabA data as an array:
uses indexOf to add an element to the array for the "Days" value
sorts the array according to a given column sequence (editable by the user)
uses `pop to delete the "Days" element from the sorted array
uses setValues to update the TabA data range with the sorted array.
function sortData() {
var ss = SpreadsheetApp.getActiveSpreadsheet()
// TabA contains raw data
var tabA = ss.getSheetByName("TabA")
var tabALR = tabA.getLastRow()
// assume headers in row 1
// assume 5 columns of data
// assume "Frequency" is in Column D (Column 4)
var tabARange = tabA.getRange(2,1,tabALR-1,5)
// Logger.log("DEBUG: the data range = "+tabARange.getA1Notation())
var tabAValues = tabARange.getValues()
// Logger.log(tabAValues) // DEBUG
// Sheet="Frequency" contains matching integer value for text values
var freq = ss.getSheetByName("Frequency")
// get the last row of Frequence data; assume that this sheet contains other data
var Avals = freq.getRange("A1:A").getValues()
var ALastRow = Avals.filter(String).length
// note assumes header in row 1
var freqValues = freq.getRange(2,1,ALastRow-1,2).getValues()
// Logger.log(freqValues) // DEBUG
// use indexOf to find the matching string value; the integer value will be in the adjacent column
// assume the string "Frequency" is in Column D (zero-based= Column 3) of the data sheet
// push the "Days" integer value onto the data array - this will be used for sorting
for (var i=0;i<tabAValues.length;i++){
var freqIdx = freqValues.map(r => r[0]).indexOf(tabAValues[i][3])
var freqValue = freqValues[freqIdx][1]
// Logger.log("DEBUG: freqIdx = "+freqIdx+", Frequency: "+tabAValues[i][3]+", number value = "+freqValues[freqIdx][1])
// push the integer value onto the data array - this will be used for sorting
tabAValues[i].push(freqValue)
}
// sort the array
var sortSeq = [6,1,2,5,3]
for (var s = 0;s<sortSeq.length;s++){}
tabAValues.sort(function(x,y){
var xp = x[s];
var yp = y[s];
return xp == yp ? 0 : xp < yp ? -1 : 1;
});
// make a copy of the updated data array
var sortArray = new Array
sortArray = tabAValues
// delete the "Days" value from the array
// returns the array
for (p=0;p<tabAValues.length;p++){
sortArray[p].pop()
}
// update the data on TabA for the sorted values
tabARange.setValues(sortArray)
}
BEFORE
AFTER

Google App Script: Remove blank rows from range selection for sorting

I want to sort real-time when a number is calculated in a "Total" column, which is a sum based on other cells, inputted by the user. The sort should be descending and I did achieve this functionality using the following:
function onEdit(event){
var sheet = event.source.getActiveSheet();
var range = sheet.getDataRange();
var columnToSortBy = 6;
range.sort( { column : columnToSortBy, ascending: false } );
}
It's short and sweet, however empty cells in the total column which contain the following formula, blanking itself if the sum result is a zero, otherwise printing the result:
=IF(SUM(C2:E2)=0,"",SUM(C2:E2))
It causes these rows with an invisible formula to be included in the range selection and upon descending sort, they get slapped up top for some reason. I want these blank rows either sorted to the bottom, or in an ideal scenario removed from the range itself (Without deleting them and the formula they contain from the sheet) prior to sorting.
Or maybe some better way which doesn't require me dragging a formula across an entire column of mostly empty rows. I've currently resorted to adding the formula manually one by one as new entries come in, but I'd rather avoid this.
EDIT: Upon request find below a screenshot of the sheet. As per below image, the 6th column of total points needs to be sorted descending, with winner on top. This should have a pre-pasted formula running lengthwise which sums up the preceding columns for each participant.
The column preceding it (Points for Tiers) is automatically calculated by multiplying the "Tiers" column by 10 to get final points. This column could be eliminated and everything shifted once left, but it's nice to maintain a visual of the actual points awarded. User input is entered in the 3 white columns.
You want to sort the sheet by the column "F" as the descending order.
You want to sort the sheet by ignoring the empty cells in the column "F".
You want to move the empty rows to the bottom of row.
You don't want to change the formulas at the column "F".
You want to achieve this using Google Apps Script.
If my understanding is correct, how about this answer?
Issue and workaround:
In the current stage, when the empty cells are scattered at the column "F", I think that the built-in method of "sort" of Class Range cannot be directly used. The empty cells are moved to the top of row like your issue. So in this answer, I would like to propose to use the sort method of JavaScript for this situation.
Modified script:
In order to run this function, please edit a cell.
function onEdit(event){
const columnToSortBy = 6; // Column "F"
const headerRow = 1; // 1st header is the header row.
const sheet = event.source.getActiveSheet();
const values = sheet.getRange(1 + headerRow, 1, sheet.getLastRow() - headerRow, sheet.getLastColumn())
.getValues()
.sort((a, b) => a[columnToSortBy - 1] > b[columnToSortBy - 1] ? -1 : 1)
.reduce((o, e) => {
o.a.push(e.splice(0, columnToSortBy - 1));
e.splice(0, 1);
if (e.length > 0) o.b.push(e);
return o;
}, {a: [], b: []});
sheet.getRange(1 + headerRow, 1, values.a.length, values.a[0].length).setValues(values.a);
if (values.b.length > 0) {
sheet.getRange(1 + headerRow, columnToSortBy + 1, values.b.length, values.b[0].length).setValues(values.b);
}
}
In this sample script, it supposes that the header row is the 1st row. If in your situation, no header row is used, please modify to const headerRow = 0;.
From your question, I couldn't understand about the columns except for the column "F". So in this sample script, all columns in the data range except for the column "F" is replaced by sorting. Please be careful this.
Note:
Please use this sample script with enabling V8.
References:
sort(sortSpecObj)
sort()
Added:
You want to sort the sheet by the column "F" as the descending order.
You want to sort the sheet by ignoring the empty cells in the column "F".
You want to move the empty rows to the bottom of row.
In your situation, there are the values in the column "A" to "F".
The formulas are included in not only the column "F", but also other columns.
You don't want to change the formulas.
You want to achieve this using Google Apps Script.
From your replying and updated question, I could understand like above. Try this sample script:
Sample script:
function onEdit(event){
const columnToSortBy = 6; // Column "F"
const headerRow = 1; // 1st header is the header row.
const sheet = event.source.getActiveSheet();
const range = sheet.getRange(1 + headerRow, 1, sheet.getLastRow() - headerRow, 6);
const formulas = range.getFormulas();
const values = range.getValues().sort((a, b) => a[columnToSortBy - 1] > b[columnToSortBy - 1] ? -1 : 1);
range.setValues(values.map((r, i) => r.map((c, j) => formulas[i][j] || c)));
}
A much simpler way to fix this is to just change
=IF(SUM(C2:E2)=0,"",SUM(C2:E2))
to
=IF(SUM(C2:E2)=0,,SUM(C2:E2))
The cells that are made blank when the sum is zero will then be treated as truly empty and they will be excluded from sort, so only cells with content will appear sorted at the top of the sheet.
Why your original formula doesn't work that way is because using "" actually causes the cell contain content so it's not treated as a blank cell anymore. You can test this by entering ISBLANK(F1) into another cell and check the difference between the two formulas.

How can I write a script to auto sort a column with formulas and update every time a change is made?

I'm tracking the percentage of different stocks in column 3 and I want them to be automatically sorted by descending order every time the data is updated. The data in column 3 is formulaic. They are not text. How can I write this script?
Spreadsheet Here
tested on your sheet and works:
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("TOP PICKS");
var range = sheet.getRange("A2:Z");
function onOpen(e) {
range.sort([{column: 3, ascending: true}]);
}

How do I send autosorted blank rows to the bottom?

I have a script that sorts Column B on edit, but there are two problems with it.
1 - It sends the rows with values to the bottom of the sheet.
2 - The numbers do not sort correctly. They should go in the order of 1,3,4,5,and 20, but when it sorts itself, it orders them as 1, 20, 3, 4, 5. It's like it only recognizes the 2 in 20 and places it after 1.
I've searched forum after forum trying to figure this out with no luck so help would be greatly appreciated.
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("050")
var range = sheet.getRange("A6:L200");
// Sorts by the values in column 2 (B)
range.sort({column: 2, ascending: true});
}
I don't know if this makes a difference or not, but the sheet that's being sorted uses VLOOKUP. Each Column from B on uses VLOOKUP.
=IFERROR(VLOOKUP(A6,data2019,3,FALSE),"")
First of all, there's no need to get the blank values in the first place, instead you can use getDataRange() to only get the range you need to sort.
Once you've got your range defined, you can sort it. Your values are not being sorted correctly (this is likely due to formatting of the cell from the VLOOKUP). You can simply set the format of the data range to number format using setNumberFormat('0') then sort the data to the order you're expecting.
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("050");
var range = sheet.getDataRange();
var numRows = range.getNumRows();
//sets the formatting of column B to a number in all populated rows
sheet.getRange(1, 2, numRows).setNumberFormat('0');
//sorts range by column B using newly formatted values
range.sort({column: 2, ascending: true});
}

Sort a range or array based on two columns that contain the date and time

Currently I'm trying to create a Google Apps Script for Google Sheets which will allow adding weekly recurring events, batchwise, for upcoming events. My colleagues will then make minor changes to these added events (e.g. make date and time corrections, change the contact person, add materials neccessary for the event and so forth).
So far, I have written the following script:
function CopyWeeklyEventRows() {
var ss = SpreadsheetApp.getActiveSheet();
var repeatingWeeks = ss.getRange(5,1).getValue(); // gets how many weeks it should repeat
var startDate = ss.getRange(6, 1).getValue(); // gets the start date
var startWeekday = startDate.getDay(); // gives the weekday of the start date
var regWeek = ss.getRange(9, 2, 4, 7).getValues(); // gets the regular week data
var regWeekdays = new Array(regWeek.length); // creates an array to store the weekdays of the regWeek
var ArrayStartDate = new Array(startDate); // helps to store the We
for (var i = 0; i < regWeek.length; i++){ // calculates the difference between startWeekday and each regWeekdays
regWeekdays[i] = regWeek[i][1].getDay() - startWeekday;
Logger.log(regWeekdays[i]);
// Add 7 to move to the next week and avoid negative values
if (regWeekdays[i] < 0) {
regWeekdays[i] = regWeekdays[i] + 7;
}
// Add days according to difference between startWeekday and each regWeekdays
regWeek[i][0] = new Date(ArrayStartDate[0].getTime() + regWeekdays[i]*3600000*24);
}
// I'm struggling with this line. The array regWeek is not sorted:
//regWeek.sort([{ column: 1, ascending: true }]);
ss.getRange(ss.getLastRow() + 1, 2, 4, 7).setValues(regWeek); // copies weekly events after the last row
}
It allows to add one week of recurring events to the overview section of the spreadsheet based on a start date. If the start date is a Tuesday, the regular week is added starting from a Tuesday. However, the rows are not sorted according to the dates:
.
How can the rows be sorted by ascending date (followed by time) before adding them to the overview?
My search for similar questions revealed Google Script sort 2D Array by any column which is the closest hit I've found. The same error message is shown when running my script with the sort line. I don't understand the difference between Range and array yet which might help to solve the issue.
To give you a broader picture, here's what I'm currently working on:
I've noticed that the format will not necessarily remain when adding
new recurring events. So far I haven't found the rule and formatted by
hand in a second step.
A drawback is currently that the weekly recurring events section is
fixed. I've tried to find the last filled entry and use it to set the
range of regWeek, but got stuck.
Use the column A to exclude recurring events from the addition
process using a dropdown.
Allow my colleagues to add an event to the recurring events using a
dropdown (e.g. A26). This event should then be added with sorting to
the right day of the week and start time. The sorting will come in
handy.
Thanks in advance for your input regarding the sorting as well as suggestions on how to improve the code in general.
A demo version of the spreadsheet
UpdateV01:
Here the code lines which copy and sort (first by date, then by time)
ss.getRange(ss.getLastRow()+1,2,4,7).setValues(regWeek); // copies weekly events after the last row
ss.getRange(ss.getLastRow()-3,2,4,7).sort([{column: 2, ascending: true}, {column: 4, ascending: true}]); // sorts only the copied weekly events chronologically
As #tehhowch pointed out, this is slow. Better to sort BEFORE writing.
I will implement this method and post it here.
UpdateV02:
regWeek.sort(function (r1, r2) {
// sorts ascending on the third column, which is index 2
return r1[2] - r2[2];
});
regWeek.sort(function (r1, r2) {
// r1 and r2 are elements in the regWeek array, i.e.
// they are each a row array if regWeek is an array of arrays:
// Sort ascending on the first column, which is index 0:
// if r1[0] = 1, r2[0] = 2, then 1 - 2 is -1, so r1 sorts before r2
return r1[0] - r2[0];
});
UpdateV03:
Here an attempt to repeat the recurring events over several weeks. Don't know yet how to include the push for the whole "week".
// Repeat week for "A5" times and add to start/end date
for (var j = 0; j < repeatingWeeks; j++){
for (var i = 0; i < numFilledRows; i++){
regWeekRepeated[i+j*6][0] = new Date(regWeek[i][0].getTime() + j*7*3600000*24); // <-This line leads to an error message
regWeekRepeated[i+j*6][3] = new Date(regWeek[i][3].getTime() + j*7*3600000*24);
}
}
My question was answered and I was able to make the code work as intended.
Given your comment - you want to sort the written chunk - you have two methods available. One is to sort written data after writing, by using the Spreadsheet service's Range#sort(sortObject) method. The other is to sort the data before writing, using the JavaScript Array#sort(sortFunction()) method.
Currently, your sort code //regWeek.sort([{ column: 1, ascending: true }]); is attempting to sort a JavaScript array, using the sorting object expected by the Spreadsheet service. Thus, you can simply chain this .sort(...) call to your write call, as Range#setValues() returns the same Range, allowing repeated Range method calling (e.g. to set values, then apply formatting, etc.).
This looks like:
ss.getRange(ss.getLastRow() + 1, 2, regWeek.length, regWeek[0].length)
.setValues(regWeek)
/* other "chainable" Range methods you want to apply to
the cells you just wrote to. */
.sort([{column: 1, ascending: true}, ...]);
Here I have updated the range you access to reference the data you are attempting to write - regWeek - so that it is always the correct size to hold the data. I've also visually broken apart the one-liner so you can better see the "chaining" that is happening between Spreadsheet service calls.
The other method - sorting before writing - will be faster, especially as the size and complexity of the sort increases. The idea behind sorting a range is you need to use a function that returns a negative value when the first index's value should come before the second's, a positive value when the first index's value should come after the second's, and a zero value if they are equivalent. This means a function that returns a boolean is NOT going to sort as one thinks, since false and 0 are equivalent in Javascript, while true and 1 are also equivalent.
Your sort looks like this, assuming regWeek is an array of arrays and you are sorting on numeric values (or at least values which will cast to numbers, like Dates).
regWeek.sort(function (r1, r2) {
// r1 and r2 are elements in the regWeek array, i.e.
// they are each a row array if regWeek is an array of arrays:
// Sort ascending on the first column, which is index 0:
// if r1[0] = 1, r2[0] = 2, then 1 - 2 is -1, so r1 sorts before r2
return r1[0] - r2[0];
});
I strongly recommend reviewing the Array#sort documentation.
You could sort the "Weekly Events" range before you set the regWeek variable. Then the range would be in the order you want before you process it. Or you could sort the whole "Overview" range after setting the data. Here's a quick function you can call to sort the range by multiple columns. You can of course tweak it to sort the "Weekly Events" range instead of the "Overview" range.
function sortRng() {
var ss = SpreadsheetApp.getActiveSheet();
var firstRow = 22; var firstCol = 1;
var numRows = ss.getLastRow() - firstRow + 1;
var numCols = ss.getLastColumn();
var overviewRng = ss.getRange(firstRow, firstCol, numRows, numCols);
Logger.log(overviewRng.getA1Notation());
overviewRng.sort([{column: 2, ascending: true}, {column: 4, ascending: true}]);
}
As for getting the number of filled rows in the Weekly Events section, you need to search a column that will always have data if any row has data (like the start date column b), loop through the values and the first time it finds a blank, return that number. That will give you the number of rows that it needs to copy. Warning: if you don't have at least one blank value in column B between the Weekly Events section and the Overview section, you will probably get unwanted results.
function getNumFilledRows() {
var ss = SpreadsheetApp.getActiveSheet();
var eventFirstRow = 9; var numFilledRows = 0;
var colToCheck = 'B';//the StartDate col which should always have data if the row is filled
var vals = ss.getRange(colToCheck + eventFirstRow + ":" + colToCheck).getValues();
for (i = 0; i < vals.length; i++) {
if (vals[i][0] == '') {
numFilledRows = i;
break;
}
}
Logger.log(numFilledRows);
return numFilledRows;
}
EDIT:
If you just want to sort the array in javascript before writing, and you want to sort by Start Date first, then by Time of day, you could make a temporary array, and add a column to each row that is date and time combined. array.sort() sorts dates alphabetically, so you would need to convert that date to an integer. Then you could sort the array by the new column, then delete the new column from each row. I included a function that does this below. It could be a lot more compact but I thought it might be more legible like this.
function sortDates() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var vals = ss.getActiveSheet().getRange('B22:H34').getDisplayValues(); //get display values because getValues returns time as weird date 1899 and wrong time.
var theDate = new Date(); var newArray = []; var theHour = ''; var theMinutes = '';
var theTime = '';
//Create a new array that inserts date and time as the first column in each row
vals.forEach(function(aRow) {
theTime = aRow[2];//hardcoded - assumes time is the third column that you grabbed
//get the hours (before colon) as a number
theHour = Number(theTime.substring(0,theTime.indexOf(':')));
//get the minutes(after colon) as a number
theMinutes = Number(theTime.substring(theTime.indexOf(':')+1));
theDate = new Date(aRow[0]);//hardcoded - assumes date is the first column you grabbed.
theDate.setHours(theHour);
theDate.setMinutes(theMinutes);
aRow.unshift(theDate.getTime()); //Add the date and time as integer to the first item in the aRow array for sorting purposes.
newArray.push(aRow);
});
//Sort the newArray based on the first item of each row (date and time as number)
newArray.sort((function(index){
return function(a, b){
return (a[index] === b[index] ? 0 : (a[index] < b[index] ? -1 : 1));
};})(0));
//Remove the first column of each row (date and time combined) that we added in the first step
newArray.forEach(function(aRow) {
aRow.shift();
});
Logger.log(newArray);
}

Resources