Excel.js custom function caller cell number format - excel

I'm trying to number format the caller cell for a custom function, specifically to replace the scientific notation with a numeric format for big numbers and then auto fit the column width.
An idea is to check the cell text for presence of "E", but the issue is that the formatting code seems to run before the result is written to the cell (which kind of makes sense, honestly), so I'm doing a comparison and set the cell format accordingly. Setting the cell format works fine (it doesn't need the result written to the cell), but auto fitting the column width doesn't.
Here is the custom function code:
getData returns a number (or an error string) from an API call
formatNumber should set the cell number format and autofit the column width, based on the returned number.
async function Test(symbol, metric, date, invocation) {
const address = invocation.address;
return await getData(symbol, metric, date)
.then(async (result) => {
if (!isNaN(result) && result > 99999999999) {
await formatNumber(address);
}
return result;
})
.catch((error) => {
console.log("error: " + error);
return error;
});
}
Here is the formatNumber code.
range.text returns #BUSY, which means the data is still retrieved from the API when the function runs. Due to this, autofitColumns will set the column size based on "#BUSY" string length.
async function formatNumber(address) {
await Excel.run(async (context) => {
const formats = [["#,##0"]];
const range = context.workbook.worksheets.getActiveWorksheet().getRange(address);
range.load("text");
await context.sync();
console.log("range.text: " + range.text);
range.load("numberFormat");
await context.sync();
range.numberFormat = formats;
range.format.autofitColumns();
await context.sync();
});
}
Any ideas?
Thank you for your time,
Adrian

The custom functions return value will be set to the cell after the function is returned.
I suggest your add-in register an onChanged event handler on the worksheet, and call format.autofitColumns() to handle the event;

Related

Replace string for a specific column (csv file) in Nodejs

I want to replace string of a particular column from a .csv file in Nodejs
here is my .csv data:
ID,Name,openingBalance,closingBalance
"27G","HARRIS TODD",23.22,465.22
"28G","ANGELO RALPH",124.31,555.20
"28N","GRODKO STEVEN",45.22,
"29A","FOWLER ROBERT",65.25,666.00
"29G","PROVOST BRIAN",,253.11
"300","BECKMAN JUDITH",114.21,878.21
in the closingBalance column there is a blank which I need to be replace as 0.00
I am able to replace the whole data, but not for the specific column,
Can anyone please help?
I used this for replace string :
var fs = require('fs')
fs.readFile(someFile, 'utf8', function (err,data) {
if (err) {
return console.log(err);
}
var result = data.replace(/string to be replaced/g, 'replacement');
fs.writeFile(someFile, result, 'utf8', function (err) {
if (err) return console.log(err);
});
});
You can use regular expression to replace blank cells, but that's easy only for first/last columns:
data.replace(/,$/gm, ',0.00');
See Regex101 for further details and playground
Other way is to parse CSV to AoA:
const csv = `ID,Name,openingBalance,closingBalance
"27G","HARRIS TODD",23.22,465.22
"28G","ANGELO RALPH",124.31,555.20
"28N","GRODKO STEVEN",45.22,
"29A","FOWLER ROBERT",65.25,666.00
"29G","PROVOST BRIAN",,253.11
"300","BECKMAN JUDITH",114.21,878.21`;
const aoa = csv
.split(/\r?\n/g)
.map((row) => {
let [ID, Name, openingBalance, closingBalance] = row.split(',');
// Fix empty values for "openingBalance" column
if (!openingBalance.trim()) {
openingBalance = '0.00';
}
// Fix empty values for "closingBalance" column
if (!closingBalance.trim()) {
closingBalance = '0.00';
}
return [ID, Name, openingBalance, closingBalance]
});
// now you have AoA with fixed values
console.log(aoa.map((row) => row.join(',')).join('\n'))
With this way, you can pre-moderate any column with any code.
You can use regex to find out blank cells for closingBalance column.
The cells for this particular column is the last one in each record which can be easily found by \n and $ in regex.
So to do this:
const result = data.replace(/(,)(?=(\n|$))/g, '$10.00');
of if you want to find out any blank cells, you can use the following regex:
/(,)(?=(,|\n|$))/g

Error in copy-pasting(in loop) to multiple Excel ranges online, using office.js

I am trying to populate multiple ranges using a formula and then convert the range to values using paste as value. The Office add-in is being used on Sharepoint Excel for the web. The code usually works but once in a while I get a "Rich API: An internal error has occurred" error, due to which the formulas do not get replaced by values. After the first time, the error happens on every subsequent try it crashes with the "Rich API: Timeout" error. There are about 300 ranges of size approx 25x25.
Code:
async function loadValues() {
//This function is exectued to fill some ranges after data is retrived from server and pasted in a bacckend table
await Excel.run(async function main(context) {
context.workbook.application.calculationMode = "Manual";
let names = context.workbook.names
context.application.suspendScreenUpdatingUntilNextSync();
var rng = names.getItem("controlsToUse").getRange();
rng.load("values");
await context.sync();
context.application.suspendScreenUpdatingUntilNextSync();
// Controls to use contains the name of the ranges in which data has to be loaded and the ranges from which formula to load data has to be copied
var controlsToUse = rng.values;
for (i = 0; i < controlsToUse.length; i++) {
// str is the range in which data has to be pasted and str1 is the range from which formula has to be copied
var str = controlsToUse[i][0];
var str1 = controlsToUse[i][1];
var range1 = names.getItem(str).getRange();
range1.copyFrom(str1, Excel.RangeCopyType.formulas);
range1.untrack();
}
await context.sync();
context.workbook.application.calculate();
await context.sync();
context.application.suspendScreenUpdatingUntilNextSync();
for (i = 0; i < controlsToUse.length; i++) {
var str = controlsToUse[i][0];
var range1 = names.getItem(str).getRange();
range1.copyFrom(str, Excel.RangeCopyType.values);
range1.untrack();
}
await context.sync();
context.workbook.application.calculationMode = "Automatic";
await context.sync();
}).catch(errorHandler)
}
Can you comment out context.runtime.enableEvents = false; and check if it works?

Office.js | Excell add in | get selected cell address on click

I am working on a Excel Web Add-In using Office.js. I need to get selected cell's Address with below scenario:
Open the Excel, Don't load add-in, click on any Cell in excel worksheet area. eg:A4
Load the add-in.
Again click on the same cell number eg:A4 that was highlighted on worksheet area as before loading add-in we selected.
Its not triggering the below code:
Office.context.document.addHandlerAsync(Office.EventType.DocumentSelectionChanged,
function (eventArgs) {
Excel.run(function (ctx) {
var range = ctx.workbook.getSelectedRange();
range.load(['address', 'values']);
return ctx.sync().then(function () {
showNotification("", range.values[0][0] + " Address:" + range.address);
});
});
});
NOTE: If I select cell Id other than previously selected cell id eg.A5 its working. Even again if I try to select from cell id A5 to previously selected cell id A4 its working.
Only if we try to select same cell its not triggering the event.
Spend lots of time can some one please help or its limitation in microsoft excel?
I think this is by design, as you are listening to SelectionChanged event, if the current selection is A4, and you click A4, it would not trigger the event as you didn't change the selection.
I am not sure your scenario, if you want to show the current selection, you could add the code before register the event. here is a sample code
async function run() {
await Excel.run(async (context) => {
var range = context.workbook.getSelectedRange();
range.load();
await context.sync();
console.log(range);
Office.context.document.addHandlerAsync(Office.EventType.DocumentSelectionChanged, showSelection);
});
}
async function showSelection(){
Excel.run(async (context) => {
var range = context.workbook.getSelectedRange();
range.load();
await context.sync();
console.log(range);
});
}

How to check if cell if empty in Office.js

I have just started with Office Addins and I'm experimenting with the functionalities. I have several VBA Userforms that I would want to replace with popups from the Office add-in.
I am using the following code to enter a string into a cell(nothing fancy, I know) but I would want to check if the cell if empty before passing the value. If it is, enter (arg.message).
the problem I have encountered:
with if (range.value == "") the value is being set in "A4" even if "A3" if empty;
with if (range.value == " ") the value is not being entered in any cells.
Can anyone give me an example of how to check if a cell is empty?
I know it seems trivial but I have only found examples of how to check with col and row numbers for conditional formatting. I am trying to test all these functionalities to be able to start moving stuff from VBA to OfficeJS.
Thanks,
Mike
function processMessage(arg) {
console.log(arg.message);
$('#user-name').text(arg.message);
dialog.close();
Excel.run(function (context) {
var sheet = context.workbook.worksheets.getItem("Sheet1");
var range = sheet.getRange("A3");
if (range.value == "") {
range.values = (arg.message);
range.format.autofitColumns();
return context.sync();
} else {
range.getOffsetRange(1, 0).values = (arg.message)
return context.sync();
}
}).catch(errorHandler);
}
PS: the whole code in case there is something wrong somewhere else
(function () {
"use strict";
// The initialize function must be run each time a new page is loaded.
Office.initialize = function (reason) {
$(document).ready(function () {
// Add a click event handler for the button.
$('#popup-button').click(opensesame);
$('#simple-button').click(function () {
Office.context.document.getSelectedDataAsync(Office.CoercionType.Text,
function (result) {
if (result.status === Office.AsyncResultStatus.Succeeded) {
$("#banner-text").text('The selected text is: "' + result.value + '"');
$("#banner").show(result.value);
console.log()
} else {
$("#banner-text").text('Error: ' + result.error.message);
$("#banner").show();
}
});
});
$("#banner-close").click(function () { $("#banner").hide(); });
$("#banner").hide();
});
}
let dialog = null;
function opensesame() {
Office.context.ui.displayDialogAsync(
'https://localhost:3000/popup.html',
{ height: 35, width: 25 },
function (result) {
dialog = result.value;
dialog.addEventHandler(Microsoft.Office.WebExtension.EventType.DialogMessageReceived, processMessage);
}
);
}
function processMessage(arg) {
console.log(arg.message);
$('#user-name').text(arg.message);
dialog.close();
Excel.run(function (context) {
var sheet = context.workbook.worksheets.getItem("Sheet1");
var range = sheet.getRange("A3");
if (range.value == "") {
range.values = (arg.message);
range.format.autofitColumns();
return context.sync();
} else {
range.getOffsetRange(1, 0).values = (arg.message)
return context.sync();
}
}).catch(errorHandler);
}
})();
The Range object has a values property, but not a value property. So range.value in your condition test is undefined which does not match an empty string; hence the else clause runs.
A couple of other things:
Your condition tries to read a property of the range object. You have to load the property and call context.sync before you can read the property.
The value of the range.values property is a two-dimensional array (although it may have a single value in it if the range is a single cell). It is not a string, so comparing it with an empty string will always be false.
If I understand your goal, I think you should be testing with whether range.values (after you load it and sync) has an empty string in it's only cell. For example, if (range.values[0][0] === ""). Even better from a performance standpoint is to load the range.valueTypes property (and sync) and then compare like this: if (range.valueTypes[0][0] === Excel.RangeValueType.empty).

Append the formula of the selected cell

I want to realise the following scenario: a user selects a cell holding a formula, clicks on the test button of my add-in, then my test function reads the formula of the selected cell, append +RAND() to it, and write it back to the workbook.
The following code reads well the formula of the selected cell, but it does not write back well. I am not sure if (the second) return ctx.sync() is correctly used.
Additionally, I don't know if I should use getSelectedDataAsync and setSelectedDataAsync (rather than getSelectedRange) in the whole scenario.
Could anyone help?
(function() {
"use strict";
Office.initialize = function(reason) {
$(document).ready(function() {
app.initialize();
$('#test').click(test);
});
}
;
function test() {
Excel.run(function(ctx) {
var selectedRange = ctx.workbook.getSelectedRange();
selectedRange.load(["formulas"]);
return ctx.sync().then(function() {
console.log(selectedRange.formulas[0][0]);
var x = selectedRange.formulas[0][0] + "+RAND()";
selectedRange.formulas[0][0] = x;
return ctx.sync();
})
}).then(function() {
console.log("done");
}).catch(function(error) {
console.log("Error: " + error);
});
}
})();
The bug is that you are trying to assign to an individual element in the formula context object. Instead use:
selectedRange.formulas = x;
or
selectedRange.formulas = [[x]];

Resources