I am trying to use openpyxl delete_rows() to delete a row from a spreadsheet. The content of the row is removed fine, and the value from the following rows are moved ahead. However, the hyperlink of the following rows are not adjusted accordingly. This will cause value and hyperlink not consistent.
An example to duplicate this problem
from openpyxl import Workbook, load_workbook
destExcelName = 'd:\Temp\hello.xlsx'
wb = load_workbook(destExcelName)
ws = wb.active
ws['A1'].value = 'Google'
ws['A1'].hyperlink = 'https://www.google.com'
ws['A1'].style = 'Hyperlink'
ws['A2'].value = 'Apple'
ws['A2'].hyperlink = 'https://www.apple.com/'
ws['A2'].style = 'Hyperlink'
ws['A3'].value = 'StackOverflow'
ws['A3'].hyperlink = 'https://stackoverflow.com'
ws['A3'].style = 'Hyperlink'
ws.delete_rows(2)
wb.save(destExcelName)
wb.close()
After delete row 2 (the apple line). StackOverflow line is moved ahead and become the new row 2. However, its hyperlink still remains on A3, and A2 don't have a hyperlink value.
Anything can be done to overcome this limitation?
Related
Trying to ditch Excel for Google sheets.
I have this empty table with some colored cells that I need to fill with symbols. Presently I use this VBA script to do the job:
Sub mark()
Dim r As Range
Dim rCell As Range
Set r = Selection.Cells
For Each rCell In r
If rCell.Interior.ColorIndex = 10 Then rCell.Value = "×"
If rCell.Interior.ColorIndex = 3 Then rCell.Value = "×"
If rCell.Interior.ColorIndex = 45 Then rCell.Value = "×"
If rCell.Interior.ColorIndex = 1 Then rCell.Value = "×"
If rCell.Interior.ColorIndex = 15 Then rCell.Value = "×"
Next
End Sub
Is there a way to accomplish same thing using google sheets?
Solution
In order to achieve this you will have to use Google Apps Script. You can attach an Apps Script project to your Google Spreadsheet by navigating Tools > Script Editor.
You should find a template function called myFunction, a perfect starting point for your script.
Here you can start translating your VBA script to Apps Script which is very similar to Javascript.
First you should define some constants for your script:
// An array containing the color codes you want to check
const colors = ['#00ff00']; // Watch out, it's case sensitive
// A reference to the attached Spreadsheet
const ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('SheetName'); // Selecting the Worksheet we want to work with by name
// Here we retrieve the color codes of the backgrounds of the range we want to check
const range = ss.getDataRange().getBackgrounds(); // Here I select all the cells with data in them
Let's now loop through our range rows and columns to apply the logic:
The .getBackgrounds() method returns a multidimensional array in the form array[row][column] -> "background-color-code".
for (let i = 0; i<range.length; i++) {
let row = range[i];
// Let's loop through the row now
for (let j = 0; j< row.length; j++) {
let color = row[j];
// If the background color code is among the ones we are checking we set the cell value to "x"
if(colors.includes(color)) {
// Javascript index notation is 0 based, Spreadsheet one though, starts from 1
ss.getRange(i+1, j+1).setValue("x"); // Let's add 1 to our indexes to reference the correct cell with the .getRange(row, column) function
}
}
}
Reference
Please take a look at the documentation for further reading and method specifications
Google Apps Script Spreadsheet Service
Range Class
.getBackgrounds()
.getRange(row,column)
How my sheet works
I'm making a spreadsheet to show how much parts I have. By using a dropdown, am I able to show that I created a product. With conditional formatting I am showing that having 0 items isn't an issue when the product is created. Created products with 0 items change from red to purple. Purple means it doesn't matter to have 0 items from this product.
My issue
My issue starts with my dropdown. If I merge cells, The value will go into the upperleft cell. This means other cells inside the merged cell are blank. This gives me a problem with conditional formatting.
My conditional formatting code example:
=if($D2=0;$E2="Created")
I have to change this code for every cell because of the condition combined with a dropdown. Having more than 250 rows would be inhumanly hard to do by hand.
My questions
Are there ways to give all cells of a merged cell the value of the combined cell in an efficient way?
Is there a better way to make my conditional formatting code applyable to merged cells?
This is my sheet
Product items collected sheet link (Shows the problem and solution!)
Product items collected sheet image (Version 1)
Product items collected sheet image (Version 2)
At the heart of this question is the operation of merged cells. When a cell is merged, say over several rows, only the cell at the top left of the merged cell can contain data, respond to conditional formatting, and so on. In a manner of speaking the other cells cease to exist and values CANNOT be assign to them.
The questioner asks:
Q: Are there ways to give all cells of a merged cell the value of the combined cell in an efficient way?
A: No. Not just in an "efficient" way; it's just not possible.
Q: Is there a better way to make my conditional formatting code applicable to merged cells?
A: No and yes ;)
No. In so far as a merged cell is concerned, everything is driven by the value in the top cell of the merged range. There are no other options for the "rest" of the merged cell.
Yes. I'd create a "helper" cells in Column F as in this screenshot
The code to achieve this is dynamic - it will automatically adapt to adding more products, more items, etc.
The logic is fairly simple: Start in F2, test whether E2 has a value (that is, is it the top of the merged cell?). If yes, then assign the value of E2 to F2 AND put that value in a variable for the following cells. If no, the cell in Column E must be part of a merged cell, so assign the value for Column F to the variable that was saved earlier.
function so5270705902() {
// basic declarations
var ss = SpreadsheetApp.getActiveSpreadsheet();
// note this is going to work on the second sheet in the spreadsheet - this can be edited.
var sheet = ss.getSheets()[1];
// Column B contains no merged cells, and always contains data (it is the BOM for the Products).
// so we'll use it to established the last row of data.
var Bvals = sheet.getRange("B1:B").getValues();
var Blast = Bvals.filter(String).length;
// Row 1 is a header row, so data commences in Row 2 - this can be edited
var dataStart = 2;
// Logger.log("the last row in column D = "+Blast);// DEBUG
// set up to loop through the rows of Column F
var mergedcellvalue = "";
for (i = dataStart; i < (Blast + 1); i++) {
// set the range for the row
var range = sheet.getRange(i, 6);
//Logger.log("row#"+i+" = "+range.getA1Notation()); DEBUG
// get the value in column E
var ECell = range.offset(0, -1);
var ECellVal = ECell.getValue();
//Logger.log("offsetrange#"+i+" range value = "+ECellVal);
//Logger.log("Column E, row#"+i+", value = "+ECell.getA1Notation()+" range value = "+ECellVal);//DEBUG
// when a row is merged, on the top row contains any data
// so we'll evaluate to see whether there is any value in this row in Column E
if (ECell.isBlank()) {
//Logger.log("ECell is blank. We're in the middle of the Merged Cell"); ??DEBUG
// Set the value to the lastes value of "mergedcellvalue"
range.setValue(mergedcellvalue);
} else {
//Logger.log("ECell has a value. We're at the top of the merged cell");//DEBUG
// paste the ECellVal into this range
range.setValue(ECellVal);
// Update the "mergedcellvalue" variable so that it can be applied against lower cells of this merged cell
mergedcellvalue = ECellVal;
} // end of the if isblank
} // end of the loop through column F
}
UPDATE 22 October 2018
For development purposes, I used a small range of only 14 rows in Column E. However the questioner's data covers over 250 rows, so I expanded development testing to cover 336 rows (yeah, I know, but I was copy/pasting and I ended up with 336 and was too lazy to delete any rows. OK?). I found that the code took over 81 seconds to process. Not good.
The primary reason (about 80 seconds worth) for the long processing time is that there is a getValue statement within the loop - var ECellVal = ECell.getValue();. This costs about 0.2 seconds per instance. Including getValue in a loop is a classic performance mistake. My bad. So I modified the code to get the values of Column E BEFORE the loop
var Evals = sheet.getRange("e2:E").getValues();.
I was surprised when the execution time stayed around the same mark. The reason was that the isBlank evaluation - if (ECell.isBlank()) { which previously took no time at all, was now consuming #0.2 second per instance. Not good++. So after searching Stack Overflow, I modified this line as follows:
if (!Evals[(i-dataStart)][0]) {.
Including setValues in a loop is also asking for trouble. An option would have been to write the values to an array and then, after the loop, update the Column E values with the array. However in this case, the execution time doesn't seem to have suffered and I'm leaving the setValues inside the loop.
With these two changes, total execution time is now 1.158 seconds. That's a percentage reduction of , um, a LOT.
function so5270705903() {
// basic declarations
var ss = SpreadsheetApp.getActiveSpreadsheet();
// note this is going to work on the second sheet in the spreadsheet - this can be edited.
var sheet = ss.getSheets()[2];
// Column B contains no merged cells, and always contains data (it is the BOM for the Products).
// so we'll use it to established the last row of data.
var Bvals = sheet.getRange("B1:B").getValues();
var Blast = Bvals.filter(String).length;
// Row 1 is a header row, so data commences in Row 2 - this can be edited
var dataStart = 2;
// Logger.log("the last row in column D = "+Blast);// DEBUG
// set up to loop through the rows of Column F
var mergedcellvalue = "";
// get the values for Column E BEFORE the loop
var Evals = sheet.getRange("e2:E").getValues();
for (i = dataStart; i < (Blast + 1); i++) {
// set the range for the row
var range = sheet.getRange(i, 6);
//Logger.log("row#"+i+" = "+range.getA1Notation()); DEBUG
// get the value in column E
var ECell = range.offset(0, -1);
var ECellVal = Evals[(i - dataStart)][0];
//Logger.log("Column E, row#"+i+", value = "+ECell.getA1Notation()+" range value = "+ECellVal);//DEBU
// when a row is merged, on the top row contains any data
// so we'll evaluate to see whether there is any value in this row in Column E
// instead is isblank, which was talking 0.2 seconds to evaluate, this if is more simple
if (!Evals[(i - dataStart)][0]) {
//Logger.log("ECell is blank. We're in the middle of the Merged Cell"); //DEBUG
// Set the value to the lastes value of "mergedcellvalue"
range.setValue(mergedcellvalue);
} else {
//Logger.log("ECell has a value. We're at the top of the merged cell");//DEBUG
// paste the ECellVal into this range
range.setValue(ECellVal);
// Update the "mergedcellvalue" variable so that it can be applied against lower cells of this merged cell
mergedcellvalue = ECellVal;
} // end of the if isblank
} // end of the loop through column F
}
UPDATE 3 March 2019
The questioner made his final changes to the code. This code is the final solution.
function reloadCreatedCells() {
// Basic declarations.
var ss = SpreadsheetApp.getActiveSpreadsheet();
// Note this is going to work on the second sheet in the spreadsheet - this can be edited.
var sheet = ss.getSheets()[1];
// Column B contains no merged cells, and always contains data (it is the BOM for the Products).
// so we'll use it to established the last row of data.
var D_vals = sheet.getRange("D1:D").getValues();
var D_last = D_vals.filter(String).length;
// First row with data.
var dataStart = 2;
// Set up to loop through the rows of Column H - K.
var mergedcellvalue = "";
// Get the values for Column H - K BEFORE the loop.
var H_K_vals = sheet.getRange("H2:K").getValues();
// How many people we have.
var people = 4;
// The first vertical row.
var rowStart = 12;
// Horizontal rows.
for (var h = 0; h < people; h++) {
// Vertical rows.
for (var v = dataStart; v < D_last; v++) {
// Set the range for the row.
var range = sheet.getRange(v, rowStart + h);
// Logger.log(range.getA1Notation()); //DEBUG
// Get the value in column H - K.
var H_K_Cell = range.offset(0, -people);
// Adding Created and not created values inside L - O.
var H_K_CellVal = H_K_vals[(v - dataStart)][h];
// Logger.log(H_K_Cell.getA1Notation() + ': ' + H_K_CellVal); //DEBUG
// When a row is merged, the value is only inside the top row.
// Therefore, you need to check if the value is empty or not.
// If the value is empty. Place the top value of the merged cell inside the empty cell.
if (!H_K_vals[(v - dataStart)][h]) {
// Logger.log(H_K_Cell.getA1Notation() + ": is blank. We're below the top cell of the merged cell."); //DEBUG
// Set the value to the top cell of the merged cell with "mergedcellvalue".
range.setValue(mergedcellvalue);
} else {
// Logger.log(H_K_Cell.getA1Notation() + ": has a value. We're at the top of the merged cell."); //DEBUG
// Paste the H_K_CellVal into this range.
range.setValue(H_K_CellVal);
// Update the "mergedcellvalue" variable, so that it can be applied against lower cells of this merged cell.
mergedcellvalue = H_K_CellVal;
} // end of the if isblank.
} // End of the vertical row loop.
} // End of the horizontal row loop.
}
I am new at this so still trying to figure how everything works.
I have a sheet that collects responses from a Google Form. Based on the answer to one of those questions I would like the row from that sheet to move to a different sheet document all together (not a sheet in the same document).
I have it set on a time based trigger every minute so as new responses come in it would kick them over to the correct document and then delete the row from the original spreadsheet.
I have been able to get part of the way there. I would like for it to take the row, columns A through E, move those to the correct document, find where the next open row is and place the data in columns A through E on the new document.
Where my issue is coming in at the moment is when the row is moved to the new document. I have formulas saved in columns G - Z on the destination page. It is finding the last row with a formula and placing the row after that (which is at the very bottom of the page). I am pretty sure this has to do with using an array? But I may be wrong. Is there a way to just have that look at the destination page column A-E, find the next blank row, and copy A-E from the original file to the new page?
arr = [],
values = sheetOrg.getDataRange().getValues(),
i = values.length;
while (--i) {
if (value1ToWatch.indexOf(values[i][1]) > -1) {
arr.unshift(values[i])
sheetOrg.deleteRow(i + 1)
sheet1in.getRange(sheet1in.getLastRow()+1, 1, arr.length, arr[0].length).setValues(arr);
};
I have multiple If statements each with some changes to the "valueToWatch" and the "Sheet1in" for different values and destination pages. If that information helps at all.
You can find the last cell in a column with data in it like this:
function findLastValueInColumn() {
var column = "A"; // change to whatever column you want to check
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
var lastDataRow = sheet.getDataRange().getLastRow();
for (var currentRow = lastDataRow; currentRow > 0; currentRow--) {
var cellName = column + currentRow;
var cellval = sheet.getRange( cellName ).getValue();
if (cellval != "") {
Logger.log("Last value in Column " + column + " is in cell " + currentRow);
break;
}
}
}
You can then use this function to figure out where to start your new data.
Spreadsheet data looks like this
function myFunction()
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName('Active Listeners');
sh.insertRowBefore(15551)
}
As i have large range of rows that could be work on. If the value of the range matches with "Apr 9" then insert row before to that. Could anyone help me to get that.
A 'for loop' to cycle through your rows from the bottom would almost do the trick. The loop inserts a row after each row specified by i. Keep in mind you'll need a different solution if your Apr 9 column is formatted as a date. This works for plain text only. You can select the column and change to plain text with "Format > Number > Plain Text" on the menu.
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName('Active Listeners');
//var shlen = sh.getDataRange().getLastRow(); //returns integer last row
var shlen = Browser.inputBox("Enter Last Row of Preferred Range", Browser.Buttons.OK_CANCEL);
var ecell = sh.getActiveCell().getA1Notation();
You may need a different dataRange (below), I've just grabbed the parameters of data in your whole sheet (above), then grabbed a range specified in A1 notation of "A1:B[number reference of bottom row]" The modification may be that you need "B1:C" + [shlen] or whichever other range.
if (shlen >= 1) {
var dataRange = sh.getRange("A1:A" + shlen).getValues();
for (var i = shlen; i > 0; i--) {
var row = dataRange[i-1];
if (row[0] == "Apr 9") {
sh.insertRowAfter(i-1)
}
}}
}
Someone more knowledgeable than me can pitch in if they have a better answer, but my only solution (which should be ok if it's a one-of) would be to just repeat the script a few times, starting at the row of your choice each time. Select cell A1 and then press "control (or command) + down arrow". It will take you to the first gap, which should be where the previous script ended. Remember the row number you're up to and plug that in the input box when you run the script again. Might take a few iterations but you'll get there.
If this process is going to be done repeatedly then best of luck in finding a solution :)
I am using Kendo Grid and using the methods suggested in this Article by Kendo. I am more interested towards the first approach as I find it faster than the 2nd approach.
The problem is if the number of records 65535, then it throws error
Invalid Row number (65536) outside allowable range (0..65535)
I am not able to find any solution to this. Done lot of research, tried the other method but that seems way too slow for my clients liking.
First you need to check if the row numbers are greater than 65535, if they are then you need to split the data in multiple sheets like so...
//create new workbook
var workbook = new HSSFWorkbook();
//create sheet
var sheet = workbook.CreateSheet();
//declare row number
int numberOfRow = 1;
//add value to sheet name inorder not to receive error that the sheet name already exists
int i = 0;
if(numberOfRow > 65535)
{
sheet = workbook.CreateSheet("(Name of sheet " + ++i + ")");
numberOfRow = 1;
//include your header row here
}