I am using below method to copy value without formula from a range of excel to the same range (basically removing formula). This range has 7 cells but they are merged.
void Copy(IRange destination, PasteType pasteType, PasteOperation pasteOperation, bool skipBlanks, bool transpose);
I am getting InvalidOperationException with message "Operation is not valid for a partial merged cell."
SpreadsheetGear.Core Version - 8.0.63.102
This works well with single cell but not with merged cells.
sourceRange.Copy(targetRange, SpreadsheetGear.PasteType.Values, SpreadsheetGear.PasteOperation.None, false, false);
This behavior is by design, as SpreadsheetGear does not allow pasting into a range that includes partial merged cells. Instead, you would need to adapt the paste operation in some way so that the destination range fully includes all cells that are part of a merged range. There are some APIs in the IRange interface that can help you determine if a given IRange is part of a merged range and return adjusted IRanges to account for this:
IRange.MergeCells - Gets or sets the property which specifies whether the top left cell of this IRange is merged.
IRange.MergeCellsDefined - Returns true if the MergeCells property of all cells represented by this IRange is false or if all cells represented by this IRange are contained by the same merged cell area, otherwise false is returned.
IRange.MergeArea - Returns an IRange representing the merged cell area if the cell represented by this single cell IRange is part of a merged cell range, otherwise the original IRange is returned.
IRange.EntireMergeArea - Returns an IRange representing the area of the current IRange which is iteratively grown as needed until it includes no partial merged cells.
Below is some sample code that demonstrates how you can use some of the above APIs to deal with your case. Note depending on the ranges involved in the copy operation (single cell vs multi-cell), the approach might vary.
// Setup a workbook with some test data and a merged range.
IWorkbook workbook = Factory.GetWorkbook();
IRange cells = workbook.ActiveWorksheet.Cells;
cells["A1:B2"].Formula = "=ROW()+COLUMN()";
cells["A1:A7"].Merge();
//For a single cell copy operation
{
IRange copyPasteRange = cells["A1"];
// If IRange.MergeCells is true, A1 will be part of a larger merged range, so we
// should use the IRange.MergeArea property to expand A1 to include the entire
// merged range
if (copyPasteRange.MergeCells)
copyPasteRange = copyPasteRange.MergeArea;
copyPasteRange.Copy(copyPasteRange, PasteType.Values, PasteOperation.None, false, false);
}
// For a multi-cell copy operation.
{
// Note in this case that A1 is part of merged range A1:A7 but B2 is not merged.
// How you deal with this depends on your requirements.
IRange copyPasteRange = cells["A1:B2"];
// One way is to use IRange.EntireMergeArea which will iteratively grow the specified
// IRange until it fully includes all merged ranges, so in this case A1:B7.
// Obviously, this means B2:B7 will be copied as a side-effect, but this is what
// would happen if you tried to copy this range in Excel's UI (i.e., when attempting
// to select A1:B1 in Excel, they will always expand the selected range to A1:B7),
// so might be acceptable in some cases.
if (copyPasteRange.Address != copyPasteRange.EntireMergeArea.Address)
copyPasteRange = copyPasteRange.EntireMergeArea; // A1:B7
copyPasteRange.Copy(copyPasteRange, PasteType.Values, PasteOperation.None, false, false);
// Alternatively, you could loop through each cell in the originalCopyPaste
// range and copy each cell as needed. I leave it up to you to flesh this out if
// it is necessary for your requirements.
if (copyPasteRange.Address != copyPasteRange.EntireMergeArea.Address)
{
foreach (IRange cell in copyPasteRange)
{
// Paste by value for each cell, taking into account merged cells however you
// see fit.
}
}
}
Related
I am a beginner in google sheets and I couldn't get around this formula. I have range of cells and I want to subtract last non empty cell to first cell (Z-A), here is the image:
As the values are updated in columns C, D, E and so on. I want to get the last non empty cell (from right) and subtract the values by moving backward (left). Like this:
sub = 10(Column G)-0(Column F)-10(Column E)-0(Column D)-10(Column C)
Can we devise a formula which will get the last non empty cell and subtract values until the first value? Here is the link to the sample sheet Thank you
try:
=LOOKUP(1, INDEX(1/(C2:F2<>"")), C2:F2)-(SUM(C2:F2)-
LOOKUP(1, INDEX(1/(C2:F2<>"")), C2:F2))
Suggestion: Use a custom function
You may use the following script as a custom function to get the difference between the value of the last cell and the sum of the other cells:
function SUBTRACTFROMLASTNUMBER(data) { //you can rename the custom function name
var sum = 0;
var data2 = data[0].filter(x => {
return (x != "") ? x : null;
}); //filtered data
var lastNumber = data2.pop(); //last number
data2.map(x => sum += x); //sums the remaining values
return (lastNumber - sum); //returns the output
}
This custom function extracts the selected data from the sheet and then separates the value of the last cell using pop() and then filters and sums the remaining data using filter() and map() and then subtracts the sum from the value of the last cell.
Usage
You may use this function as:
=SUBTRACTFROMLASTNUMBER(<range>)
Reference:
How to Manipulate Arrays in JavaScript
I am trying to create a select list in Google Sheet based on cells in another sheet. Those cells contains all the values that my list should display.
It works well but I also want to retrieve the style of those cells along with the values. So in my main sheet, depending on the selected value the style is copied from the "source" cells.
I know I can setup a conditional formatting so if the value is X or Y or Z I can apply a style but since my "source" cells are going to be updated, I'll have to also update those conditions which is a slow process.
I am wondering if there is a way to just dynamically copy the style of another cell.
Here is an example of my source cells:
Using Apps Script, you could create an onEdit trigger to do the following:
Track changes to the cells that contain data validations based on values in a range.
If the edited cell contains this data validation, look for the value in the source range that corresponds to the selected value.
Copy and paste the format of the cell in source range to the selected cell.
To do that, just create a bound script by selecting Tools > Script editor, copy the following code and save the project.
Code sample (check comments):
const onEdit = e => {
// Get information on edited cell (column, row, value):
const range = e.range;
const column = range.getColumn();
const row = range.getRow();
const value = range.getValue();
// Check that edited cell contains a validation rule and that its criteria type is "VALUE_IN_RANGE":
const validation = range.getDataValidation();
if (validation && validation.getCriteriaType() == "VALUE_IN_RANGE") {
const sourceRange = validation.getCriteriaValues()[0]; // Get range validation is based on
// In source range, get index of the cell that corresponds to selected value in edited cell:
const values = sourceRange.getValues();
let i, j;
for (i = 0; i < values.length; i++) {
j = values[i].indexOf(value);
if (j != -1) break;
}
const rangeToCopy = sourceRange.getSheet().getRange(sourceRange.getRow() + i, sourceRange.getColumn() + j); // Get cell in source range to copy format
rangeToCopy.copyFormatToRange(range.getSheet(), column, column, row, row); // Copy format to edited cell
}
}
Reference:
onEdit trigger
Class DataValidation
copyFormatToRange(sheet, column, columnEnd, row, rowEnd)
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 to google scripts. I have a range where the cells in a column change. If the number of a cell from that column is above zero then the script has to send an email with the message being the content from a cell in the same row with the updated cell(that is, different column) Let's say If a value in column I changes then automatically it has to send an email with the message contained in the same row but column F for example. Please see below, I would appreciate some help.
function SendValue() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName("Dividend");
var values = sheet.getRange("I2:I").getValues();
//var values1 = sheet.getRange("G2:G").getValues();
var results = [];
for(var i=0;i<values.length;i++){
if(values[i]>0){
results.push(sheet.getIndex([+i+2]+"G"));
// +2 because the loop start at zero and first line is the second one (I2)
}
}
MailApp.sendEmail('sxxxxxx#gmail.com', 'EX-DIVIDEND', results );
};
getValues() returns a 2 dimensional array, or more accurately an array of arrays, even if one of the dimensions is 1. values[i] is not a value but rather an array representing the i row of the range. You will need to acess that array to get at the cell at a particular column in that row.
replace if(values[i]>0) with if(values[i][0]>0)
I suspect that results.push(sheet.getIndex([+i+2]+"G")); should be results.push(sheet.getRange("G"+(i+2)).getValue()); as getIndex() doesn't take arguments and that looks like it is meant to be A1 notation.
However there is a better option than trying to construct A1Notation.
getRange() supports using numbers to refer to rows and columns. getRange(7, i+2) is a better choice.
https://developers.google.com/apps-script/reference/spreadsheet/range#getvalues
https://developers.google.com/apps-script/reference/spreadsheet/sheet#getRange(Integer,Integer)
I have an Xcode Swift application that is using a UITableView to display a list of results.
Each row has a value which is calculated to be the difference of a value in the next row.
When the data loads, all is good. However I noticed that, as I have now learnt with the behaviour of the TableView, is that each cell is calculated when it is about to be presented to view.
So originally I had in my code logic that said,
// Global variable
var previousX = 0;
// Building up cell (this happens for each cell in the table view)
----------------------------
var x = 5;
var difference = x - previousX;
previousX = x;
----------------------------
So this works fine when the load lists for the first time, and you scroll down.
However as soon as you scroll up, it loads the cells again. What this means is that my 'previousX' isn't neccessaily the cell above the current cell that I am building, but could be below to. So the values are sort of unpredicatble.
How can I avoid this? How can my cell ALWAYS get the 'previous/above' cell value when building the cell up? Clearly using 'previousX' being the value of the last cell that was populated is not the ideal solution.
If you need any more info from me, let me know.
thanks!
To Achive This you have to maintain array for This:
// instance variable
var previousX:[Int] = []
// Give default to array
previousX = [Int](count: 3, repeatedValue: 0) // create array as per you need
// Building up cell (this happens for each cell in the table view)
var x = 5
var difference = x - previousX[indexPath.row - 1]
previousX[indexPath.row] = difference
Use indexPath.row value as the data for each row which is unique and increase/decrease automatically depending on the cell position. It might help.