Excel Javascript API - join range or complex range selection - excel

In Excel macro you can do something simple like:
Range("C8:G8,C12:H12,C19:I19").Value = 1
this gives me an opportunity to create a complex range selection and reduce the number of api calls and sync queue.
But when I do
const range = activeWorkSheet.getRange("C8:G8,C12:H12,C19:I19");
range.format.fill.color = "yellow";
I get
InvalidArgument: The argument is invalid or missing or has an
incorrect format.
There is also no getJoinRange or getUnionRange I could use.
Is there a workaround? I am having some performance issue as I do thing like format a row based on odd/even.

There is a beta feature forthcoming, which will allow multi-area ranges. Its syntax is still TBD, it might be exactly what you wrote (with range being allowed to be a multi-area range), or perhaps we'll keep Range a single contiguous object and have parallel methods like worksheet.getMultiAreaRange("C8:G8, C12:H12, C19:I19) to do what you would like.
Also, what version of Office do you have and are you on Insider Fast, by any chance?
Re. performance, can you post your exact use-case as a minimal snippet? There may be some optimizations you can do, even barring multi-area ranges.
Update
If all you're doing is a 3x3 or a 5x5 (i.e., not something super-huge), you don't need multi-area support. You can just do:
await Excel.run(async (context) => {
const sheet = context.workbook.worksheets.getActiveWorksheet();
const rowCount = 5;
const columnCount = 5;
const range = sheet.getRangeByIndexes(0, 0, rowCount, columnCount);
for (let row = 0; row < rowCount; row = row + 2) {
range.getRow(row).format.fill.color = "purple";
}
await context.sync()
});

Related

Is there a google-script method like Application.Intersect(Target, Range) in Excel?

Now I understand that the question is deeper, and is connected with the tracking of events.
In Excel, I use this code:
If Not Intersect(Target, Sh.Range("$A$1:$A$300")) Is Nothing sub_do_something()
Here, Target - the address of the selected cell, Intersect determines whether the cell belongs to the specified range.
I use it in the system for filling and calculating the costing of the project.
The user clicks a row in a specific section of the calculation template. The script determines the address of the selected cell and switches the user to a specific sheet of the directory. Next, the user clicks on the desired line of the directory, the script copies a certain range of cells in the line and returns the user back to the calculation. When this happens, the copied data is inserted into a range of cells, starting with the selected one.
Thus, the creating a calculation, in which there can be more than 100 positions, is greatly simplified.
In Excel, everything works fine, but soon I plan to transfer this project to a cloud-based service, and Google Sheets is the best option.
Alas, only some events can be tracked in GAS, for example, using onOpen or onEdit triggers.
Excel has much more tracked events.
After a search on the StackOverflow, I found several similar issues related to tracing events, for example, How to find where user's cursor is in Document-bound script, Can we implement some code that fires upon selecting something in google document?, Google app script monitor spreadsheet selected ranges.
From the answers to these questions, it is clear that in GAS there is no such simple solution as Intersect(Target, Range) in Excel.
The last example uses the side menu, running a script from it that queries the sheet 5 times per second, and displays the address of the active cell in the "data" field.
Unfortunately, this code does not work for me. In the debugger, the getActiveRange() function works fine, but this code does not work:
$(document).ready(() => {
setInterval(()=>{
google.script.run.withSuccessHandler(log).getActiveRange();
},200)
})
log(e) => {
$('#data').val(e)
}
Question.
If someone did something similar, please share your experience.
Or tell me why this example does not work. If he can be reanimated, I will adapt him to solve my task.
I worked on a similar project and here's the solution:
function onSelectionChange(e)
{
var ss = e.source;
var Sh = ss.getActiveSheet();
var range = Sh.getRange("A1:A300");
var target = e.source.getActiveRange();
//check for intersection
if(RangeIntersects(target, range))
{
Logger.log("Changed Row: " + target.getRow() + "\nValue: " + target.getValue());
}
}
//returns true if target intersects with the predefined range
function RangeIntersects(target, range)
{
return (target.getLastRow() >= range.getRow()) && (range.getLastRow() >= target.getRow()) && (target.getLastColumn() >= range.getColumn()) && (range.getLastColumn() >= target.getColumn());
}
Here's an idea. I can't quite get it to work though.
Maybe someone else can give a better answer.
Also, having functions running 24/7 is not possible with GAS, I think, as there are limits to the total run-time. You may wish to add a code-guard that exits the script if the last update time is longer than 10 minutes ago or something.
function checkSelection() {
var spreadsheet = SpreadsheetApp.getActive();
var targetRange = spreadsheet.getRange('activate');
// Change your named ranged name here
var tCol = targetRange.getColumn();
var tLastCol = targetRange.getLastColumn();
var tRow = targetRange.getRow();
var tLastRow = targetRange.getLastRow();
var num = 0;
for (num; num < 115; ++num) {
// Repeats the code below 100 times
var range = spreadsheet.getActiveRange();
var row = range.getRow();
var col = range.getColumn();
if (col >= tCol && col <= tLastCol && row >= tRow && row <= tLastRow) {
range.setBackground('#000000');
// Change the code in this block to your code.
}
SpreadsheetApp.flush();
Utilities.sleep(500);
// Waits half a second before repeating
}
}
115 repetitions * 500ms wait seems to run for almost a minute, then the trigger will fire the whole function again.
Intersection of two Ranges
You can use this to calculate intersection of two ranges. It requires an object in the form of: {rg1:'A1Notation String',rg2:'A1Notation String'}
function calculateIntersection1(rgObj) {
var iObj={};
var ss=SpreadsheetApp.getActive();
var sh=ss.getActiveSheet();
var rg1=sh.getRange(rgObj.rg1);
var rg2=sh.getRange(rgObj.rg2);
var iObj={rg1colst:rg1.getColumn(),rg1colen:rg1.getColumn()+rg1.getWidth()-1,rg1rowst:rg1.getRow(),rg1rowen:rg1.getRow()+rg1.getHeight()-1,rg2colst:rg2.getColumn(),rg2colen:rg2.getColumn()+rg2.getWidth()-1,rg2rowst:rg2.getRow(),rg2rowen:rg2.getRow()+rg2.getHeight()-1};
if(iObj.rg1colst>iObj.rg2colen || iObj.rg1colen<iObj.rg2colst || iObj.rg1rowst>iObj.rg2rowen || iObj.rg1rowen<iObj.rg2rowst || iObj.rg2colst>iObj.rg1colen || iObj.rg2colen<iObj.rg1colst || iObj.rg2rowst>iObj.rg1rowen || iObj.rg2rowen<iObj.rg1rowst) {
return '<h1>No intersecting cells</h1>';
}else{
var vA1=rg1.getValues();
var v1=[];
var vA2=rg2.getValues();
var v2=[];
for(var i=0;i<vA1.length;i++){
for(var j=0;j<vA1[i].length;j++){
var s=Utilities.formatString('(%s,%s)', iObj.rg1rowst+i,iObj.rg1colst+j);
v1.push(s);
}
}
for(var i=0;i<vA2.length;i++){
for(var j=0;j<vA2[i].length;j++){
var s=Utilities.formatString('(%s,%s)', iObj.rg2rowst+i,iObj.rg2colst+j);
v2.push(s);
}
}
var oA=[];
for(var i=0;i<v1.length;i++){
var idx=v2.indexOf(v1[i]);
if(idx>-1){
oA.push(v2[idx]);
}
}
return Utilities.formatString('Intersecting Cells: %s', oA.join(', '));
}
}
It either returns the string "No Intersecting Cells" or a string identifying the intersecting cells in (row, column) format.

Excel Javascript (Office.js) - LastRow/LastColumn - better alternative?

I have been a fervent reader of StackOverflow over the last few years, and I was able to resolve pretty much everything in VBA Excel with a search and some adapting. I never felt the need to post any questions before, so I do apologize if this somehow duplicates something else, or there is an answer to this already and I couldn't find it.
Now I`m considering Excel-JS in order to create an AddIn (or more), but have to say that Javascript is not exactly my bread and butter. Over the time of using VBA, I find that one of the most simple and common needs is to get the last row in a sheet or given range, and maybe less often the last column.
I've managed to put some code together in Javascript to get similar functionality, and as it is... it works. There are 2 reasons I`m posting this
Looking to improve the code, and my knowledge
Maybe someone else can make use of the code meanwhile
So... in order to get my lastrow/lastcolumn, I use global variables:
var globalLastRow = 0; //Get the last row in used range
var globalLastCol = 0; //Get the last column in used range
Populate the global variables with the function to return lastrow/lastcolumn:
function lastRC(wsName) {
return Excel.run(function (context) {
var wsTarget = context.workbook.worksheets.getItem(wsName);
//Get last row/column from used range
var uRange = wsTarget.getUsedRange();
uRange.load(['rowCount', 'columnCount']);
return context.sync()
.then(function () {
globalLastRow = uRange.rowCount;
globalLastCol = uRange.columnCount;
});
});
}
And lastly get the value where I need them in other functions:
var lRow = 0; var lCol = 0;
await lastRC("randomSheetName");
lRow = globalLastRow; lCol = globalLastCol;
I`m mainly interested if I can return the values directly from the function lastRC (and how...), rather than go around with this solution.
Any suggestions are greatly appreciated (ideally if they don't come with stones attached).
EDIT:
I've gave up on using an extra function for this as for now, given that it uses extra context.sync, and as I've read since this post, the less syncs, the better.
Also, the method above is only good, as long your usedrange starts in cell "A1" (or well, in the first row/column at least), otherwise a row/column count is not exactly helpful, when you need the index.
Luckily, there is another method to get the last row/column:
var uRowsIndex = ws.getCell(0, 0).getEntireColumn().getUsedRange().getLastCell().load(['rowIndex']);
var uColsIndex = ws.getCell(0, 0).getEntireRow().getUsedRange().getLastCell().load(['columnIndex']);
To break down one of this examples, you are:
starting at cell "A1" getCell(0, 0)
select the entire column "A:A" getEntireColumn()
select the usedrange in that column getUsedRange() (i.e.: "A1:A12")
select the last cell in the used range getLastCell() (i.e.: "A12")
load the row index load(['rowIndex']) (for "A12" rowIndex = 11)
If your data is constant, and you don't need to check lastrow at specific column (or last column at specific row), then the shorter version of the above is:
uIndex = ws.getUsedRange().getLastCell().load(['rowIndex', 'columnIndex']);
Lastly, keep in mind that usedrange will consider formatting as well, not just values, so if you have formatted rows under your data, expect the unexpected.
late edit - you can specify if you want your used range to be of values only (thanks Ethan):
getUsedRange(valuesOnly?: boolean): Excel.Range;
I have to say a big thank you to Michael Zlatkovsky who has put a lot of work, in a lot of documentation, which I`m far from finishing to read.

Select a discontiguous range

I'm building an office-js add-in for Excel. I need to select two non-adjacent cells (e.g A1 and C3). The following code works to select the multi-cell range starting at A1 and ending at C3.
Excel.run(function (ctx) {
var sheet = ctx.workbook.worksheets.getItem('sheet1');
var range = sheet.getRange('a1:c3');
range.select();
return ctx.sync();
});
However, I'm looking to select only the two cells (A1 and C3). In VBA the syntax is
worksheets("sheet1").range("a1,c3").select
But I cannot find anything analogous in office-js. I've tried as similar syntax with office-js:
Excel.run(function (ctx) {
var sheet = ctx.workbook.worksheets.getItem('sheet1');
var range = sheet.getRange('a1,c3');
range.select();
return ctx.sync();
});
but it fails with: {"code":"InvalidArgument","message":"The argument is invalid or missing or has an incorrect format.","errorLocation":"Worksheet.getRange"}
An API to work with discontinuous ranges is not yet available in Office.js. We are working on it and are finalizing the design right now. In the meantime, you will have to create separate range objects for the two cells and operate on each of them with duplicate commands.
Actually on the latest insiders fast (16.0.9327.2006 ) deployed just a few days ago you can actually try our implementation for Areas (aka discontinuous ranges. )
please make sure to use our preview cdn to test this.(https://appsforoffice.microsoft.com/lib/beta/hosted/office.js)
but basically you can do things like:
function run() {
return Excel.run(function (context) {
var range = context.workbook.getSelectedRange();
range.load("address");
return context.sync()
.then(function () {
console.log("The range address was \"" + range.address + "\".");
});
});
}
and you will see that if you select a non-continuous range you will get something like: "Sheet1!C6:C14,Sheet1!F12:H22".
you can pass a similar string on the getRange method to create an Area and simultaneously format it etc.
Please give it a try and send us your feedback! thanks!
context.workbook.getSelectedRange()
is used for contiguous ranges.
If you want to get the range for a discontiguous range you should use:
context.workbook.getSelectedRanges()

Assign an array of values to a range

I want to initiate a range of worksheet with an array of values. I have the following code.
function initiate(address, values) {
return Excel.run(function (ctx) {
var sheet = ctx.workbook.worksheets.getActiveWorksheet();
var range = sheet.getRange(address);
var range.values = values;
return ctx.sync()
}
}
My tests show that this works only when values has exactly the same dimension (ie, height, width) as address, or when values is a single value. Otherwise, there will be an error.
Could anyone confirm that?
If so, I need to adjust address to suit the dimension of values. It is easy to get the dimension of values, but I cannot find a function to get a range from eg, 1 top-left cell + 1 number of rows + 1 number of columns, or 1 top-left cell and 1 bottom-right cell.
Am I missing something?
As CompuChip said, there is not (yet) an API for resizing a range to a particular absolute size, though it is forthcoming.
That said: if you have an array, just start with a single cell and then resize it by array's row-count-minus-1 (i.e., array.length - 1), followed by columns-minus-1 (array[0].length - 1)
const values = [
[1, 2, 3],
["Hello", "Bonjour", "Привет"]
]
await Excel.run(async (context) => {
const sheet = context.workbook.worksheets.getActiveWorksheet();
const range = sheet.getRange("D3").getResizedRange(
values.length - 1, values[0].length - 1);
range.values = values;
await context.sync();
});
You can try this snippet live in literally five clicks in the new Script Lab (https://aka.ms/getscriptlab). Simply install the Script Lab add-in (free), then choose "Import" in the navigation menu, and use the following GIST URL: https://gist.github.com/Zlatkovsky/6bc4a7ab36a81be873697cab0fa0b653. See more info about importing snippets to Script Lab.
In VBA you would use Range.Resize. In OfficeJS there seems to be a function called getResizedRange which
[g]ets a Range object similar to the current Range object, but with its bottom-right corner expanded (or contracted) by some number of rows and columns.
Unfortunately it accepts the delta values, so you need to calculate the difference between the current range size and the target size.
For example, if you wanted to obtain a range of rows by cols cells, you could try something along the lines of
var originalRange = sheet.getRange(address);
var range = originalRange.getResizedRange(
rows - originalRange.rowCount, cols - originalRange.columnCount);

Set $objValidation/Dropdown range from variable

I am attempting to have phpexcel set the range for $objValidation based off a variable so not to have null values in my dropdown. This was my code
$objValidation->setFormula1('Index!$A$5:$A'.'count(Index!$A$5:$A$200');
which resulted in additional blank/null values in my dropbox making it bigger than need be. what I would like to do is something like this
$sql_temp = "SELECT `tempID`,`serialNUM` FROM `temp_sensor_specs` WHERE `statusTYPE`='SPARE'";
$result_temp = mysqli_query($link, $sql_temp);
$row_temp = mysqli_fetch_all($result_temp,MYSQLI_NUM);
$objPHPExcel->getActiveSheet()->fromArray($row_temp,null,'A5');
$count_temp = count($row_temp) + 4;
$objValidation = $objPHPExcel->getActiveSheet()->getCell('B4')->getDataValidation();
$objValidation->setType(PHPExcel_Cell_DataValidation::TYPE_LIST);
$objValidation->setErrorStyle(PHPExcel_Cell_DataValidation::STYLE_INFORMATION);
$objValidation->setAllowBlank(true);
$objValidation->setShowDropDown(true);
$objValidation->setErrorTitle('Input error');
$objValidation->setError('Value is not in list');
$objValidation->setFormula1('Index!$A$5:$A$count_temp');
So that didn't work I've also tried it in several variations as such
$objValidation->setFormula1('Index!$A$5:$A'.'$count_temp');
$objValidation->setFormula1('Index!$A$5:$A'.count($row_temp) + 4);
$objValidation->setFormula1('Index!$A$5:$A'$count_temp);
I really feel I've used syntax incorrectly, but can't figure out how. I've done similar range setting in loops for( $i=4; $i<=15; $i++ ){
$objValidation = $objPHPExcel->getActiveSheet()->getCell('B'.$i)->getDataValidation(); but also don't think this needs to be looped it should be a simple count and set that value as the count return +4 (as my dropdown starts on cell row 5)
thanks in advance
So the proper syntax ended up being `$objValidation->setFormula1('Index!$A$5:$A'."$count_temp");

Resources