Combining multiple xlsx files into a single google sheet for datastudio - excel

I have a folder that will receive multiple xlsx files that will be uploaded via Google forms. There were will be new sheets added a couple of times a week and this data will need to be added.
I want convert all of these xlsx files into a single sheet that will feed a datastudio.
I had started working with this script:
function myFunction() {
//folder ID
var folder = DriveApp.getFolderById("folder ID");
var filesIterator = folder.getFiles();
var file;
var filetype;
var ssID;
var combinedData = [];
var data;
while(filesIterator.hasNext()){
file = filesIterator.next();
filetype = file.getMimeType();
if (filetype === "application/vnd.google-apps.spreadsheet"){
ssID = file.getId();
data = getDataFromSpreadsheet(ssID)
combinedData = combinedData.concat(data);
}//if ends here
}//while ends here
Logger.log(combinedData.length);
}
function getDataFromSpreadsheet(ssID) {
var ss = SpreadsheetApp.openById(ssID);
var ws = ss.getSheets()[0];
var data = ws.getRange("A:W" + ws.getLastRow()).getValues();
return data;
}
Unfortunately that array is returning 0! I think this maybe due to the xlsx issue.

1. Fetch the excel data
Unfortunately, Apps Script can not deal directly with excel values. You need to first convert those files into Google Sheets to access the data. This is fairly easy to do, and can be accomplished using the Drive API (you can check the documentation here) with the following two lines at the top of your code.
var filesToConvert = DriveApp.getFolderById(folderId).getFilesByType(MimeType.MICROSOFT_EXCEL);
while (filesToConvert.hasNext()){ Drive.Files.copy({mimeType: MimeType.GOOGLE_SHEETS, parents: [{id: folderId}]}, filesToConvert.next().getId());}
Please note that this duplicates the existing file by creating a Google Sheets copy of the excel but does not remove the excel file itself. Also note that you will need to activate the Drive API service.
2. Remove duplicates from combinedData
This is not as straightforward as removing duplicate from a regular array, as combinedData is an array of arrays. Nevertheless, it can be accomplished by creating an intermediate object that stores an stringified version of the row array as the key and the row array itself as the value:
var intermidiateStep = {};
combinedData.forEach(row => {intermidiateStep[row.join(":")] = row;})
var finalData = Object.keys(intermidiateStep).map(row=>intermidiateStep[row]);
Extra
I also found another mistake in your code. You should add a 1 (or whichever the first row that you want to read is) when declaring the range of the values to be read, so
var data = ws.getRange("A1:W"+ws.getLastRow()).getValues();
instead of:
var data = ws.getRange("A:W" + ws.getLastRow()).getValues();
As it currently is, Apps Script fails to understand the exact range you want to be read and just assumes that it is the whole page.

Related

Copy Excel cell value and add rows to another table

In a table (in excel) in a column I have some number(A).
I want the flow to take that number (A) and to create number of rows equels to Number (A)
For example if number(A) is 4, then in another table to be added 4 rows
I've made an assumption on the source and destination tables. This concept can be adjusted and applied to suit your own scenario.
I'd be using Office Scripts to do this. If you've never used it then feel free to consult the Microsoft documentation to get you going ...
https://learn.microsoft.com/en-us/office/dev/scripts/tutorials/excel-tutorial
This is the script you need to create (change the name of your tables accordingly) ...
function main(workbook: ExcelScript.Workbook)
{
var addRowsTable = workbook.getTable('TableRowsToAdd');
var addRowsToTable = workbook.getTable('TableAddRowsToTable');
var addRowsTableDataRange = addRowsTable.getRangeBetweenHeaderAndTotal();
var addRowsTableDataRangeValues = addRowsTableDataRange.getValues();
// Sum the values so we can determine how many more rows need to be added
// to the destination table.
var sumOfAllRowsToBeInExistence = 0;
for (var i = 0; i < addRowsTableDataRangeValues.length; i++) {
if (!isNaN(addRowsTableDataRangeValues[i][0])) {
sumOfAllRowsToBeInExistence += Number(addRowsTableDataRangeValues[i][0]);
}
}
var currentRowCount = addRowsToTable.getRangeBetweenHeaderAndTotal().getRowCount();
var rowsToAdd = sumOfAllRowsToBeInExistence - currentRowCount;
console.log(`Current row count = ${currentRowCount}`);
console.log(`Rows to add = ${rowsToAdd}`);
if (rowsToAdd > 0) {
/*
The approach below is contentious given the performance impact but this approach ...
for (var i = 1; i <= rowsToAdd; i++) {
... didn't always yield the correct result. May be a bug but needs investigation.
Ultimately, there are a few ways to achieve the same result, like using the resize method.
This was the easiest option for a StackOverflow answer.
*/
while (addRowsToTable.getRangeBetweenHeaderAndTotal().getRowCount() <
sumOfAllRowsToBeInExistence) {
addRowsToTable.addRows();
}
}
}
You can then call that from PowerAutomate using the Run script action under Excel Online (Business) ...
You can use that approach or all of the actions that are available in PowerAutomate which will achieve the same sort of thing.
IMO, Using Office Scripts is much easier. Creating a large flow can be a real pain in the backside to deal with given there'll be a whole heap of actions that you'll need to throw in to reach the same outcome.
I would pass the number of rows to add in an office scripts script as a parameter. Once you have the value, create a JSON string of a 2d array. You want to create a loop using the number of rows to add. In the loop you continue to concatenate the 2d array. Once you've exited the loop, parse the JSON string and add the 2d array to the table. You can see how you code might look below:
function main(workbook: ExcelScript.Workbook, rowsToAdd: number)
{
//set table name
let tbl = workbook.getTable("table2")
//initialize json string with open bracket
let jsonArrString = "["
//set the temp json string with a 2d array
let tempJsonArr = '["",""],'
//concatenate json string equal to the number of rows to add
for (let i = 0; i < rowsToAdd; i++){
jsonArrString += tempJsonArr
}
//remove extra comma from JSON string
jsonArrString = jsonArrString.slice(0, jsonArrString.length-1)
//add closing bracket to JSON string
jsonArrString += "]"
//parse json string into array
let jsonArr: string[][] = JSON.parse(jsonArrString)
//add array to table to add the number of rows
tbl.addRows(null,jsonArr)
}

Azure Logic App calculate Total Sum of a column from a CSV file

I'm using an Azure Logic App to get the blob contents from my storage account. The Blob-file is in .CSV file. The CSV file consists of a few columns and multiple rows. The file is basically an Invoice file which consists of a column named "PreTaxCost". I need to calculate the Total Sum of the all the entries within the "PreTaxCost" column in the CSV file to get the TOTAL BILLING COST.
Any idea on how I can achieve this using Azure Logic Apps?
Thanks!
According to our conversation and your requirements. Since you can't solve a problem when doing add number in JS inline code, so provide the add number steps below for your reference:
1. Delete the last few lines of the code in JS inline code action, return json directly.
2. Initialize two variables sum and tempItem.
3. Use "For each" to loop the Result from JS inline code action, and do set variable action in the "For each" loop.
4. The expression of fx add(...) is add(variables('tempItem'), float(items('For_each')?['PreTaxCost'])). If your "For each" loop named For each 2, the expression should be add(variables('tempItem'), float(items('For_each_2')?['PreTaxCost'])).
5. Please do not forget enable Concurrency Control, and set Degree of Parallelism as 1. Then run the logic app, you can get the sum result.
If you want to convert csv to json in Azure logic app, you can use Execute JavaScript Code action to run js code to implement it. The JS code is as below
function csvToJson(csv){
var lines=csv.split("\r\n");
var result = [];
var headers=lines[0].split(",");
for(var i=0; i<headers.length; i++){
//remove space
headers[i]=headers[i].split(" ").join("")
}
for(var i=1;i<lines.length;i++){
var obj = {};
var currentline=lines[i].split(",");
for(var j=0;j<headers.length;j++){
obj[headers[j]] = currentline[j];
}
result.push(obj);
}
return JSON.stringify(result);;
}
var content = "<csv content>";
var json = JSON.parse(csvToJson(content));
var sum = 0;
json.forEach(item=>{
sum = sum + parseInt(item.PreTaxCost);
});
return sum;
For more details, please refer to here and here

email spreadsheet rows if a cell contains a certain word - Google Spreadsheet + app script

I'm creating a "deadline reminder system" using a google spreadsheet to manage contracts and projects deadlines, here's the file: https://docs.google.com/spreadsheets/d/1CEk67cXN9NrDy_-ueOxcdwmo2dyyaX47OYIQXi5bHpA/edit#gid=1331412473
I'm trying to use Google app script to create a script that every day check every row of each sheet and send me an email with rows where last column says "in scadenza" (about to expire) or "scaduto" (expired).
For the email template I would like to revve rows as an html table or as a pdf file (what do you think is better?)
I would also find a way to avoid sending duplicates (for example adding a "reminder sent" text in stato (state) column or near this column and colour that cells.
As you can see some tables have a slightly different layout so I think I have to write down a function for each sheet since the colun range that I have to scan is different.
I'm absolutely a beginner with google script, I've found a lot of examples about how to send emails from google sheets but I can't find the correct way to put all together and write down a script that works...
I would really aprecciate if you can take a look to my spreadsheet and help me to write down the script.
Many thanks to all!
Just for reference, here some resources that I've found:
https://www.groovypost.com/howto/google-sheets-send-email-based-on-cell-value/#:~:text=Step%201%3A%20Sending%20an%20Email,that%20contains%20an%20email%20address.
How to send several rows of google sheet table via email if cell match today's date ( this example uses date as filter, I have to use the cell text content)
https://www.labnol.org/code/19869-email-google-spreadsheets-pdf
Solution
You can use a Clock trigger to run a function that filters your Sheets rows in the desired Spreadsheet according to the Stato row value.
Code
First you will need to build the filtering functions:
This function takes some rows as input ad builds a table body HTML with the filtered rows:
function getSheetValuesAsTable(values) {
var states = ['in scadenza', 'scaduto']; // Here you can add new watched statuses
return values.filter(row => states.includes(row[row.length -1]))
.map(row => "<tr>"+row.map(x => "<td>"+x+"</td>").join('')+"</tr>");
}
While the Sheet structure has the Stato column as the last column you will be able to build different tables for each one of your Sheets.
So you will need another function to build the table headers:
function getSheetHeadersAsTable(values) {
var headers = values[0];
Logger.log(values);
var tabHeaders = headers.map(header => `<th>${header}</th>`);
return `<table><tr>${tabHeaders.join('')}</tr>`;
}
Now you can wrap up the table headers and body in a HTML string and send it with the GmailApp.sendEmail() method:
function emailSpreadsheetAsHTMLTable() {
var ss = SpreadsheetApp.openById('spreadsheet-id');
var body = buildMessage(ss.getSheets());
var subject = `Scadenze Contratti/Domini/Progetti`;
var email = 'esposito.luca94#gmail.com';
if (MailApp.getRemainingDailyQuota() > 0)
GmailApp.sendEmail(email, subject,body, {
htmlBody: body
});
}
function buildMessage(sheets) {
return sheets.map(sheet => {
var values = sheet.getDataRange().getValues();
return `<h2>${sheet.getName()}</h2>${buildSheetTable(values)}</table>`; // Here I add the Sheet name for each table
}).join('');
}
function buildSheetTable(values) {
var tabHeaders = getSheetHeadersAsTable(values);
var tabBody = getSheetValuesAsTable(values);
return tabHeaders + tabBody;
}
Now you can install the Clock trigger which will call the emailSpreadsheetAsHTMLTable():
function buildClockTrigger() {
ScriptApp.newTrigger('emailSpreadsheetAsHTMLTable').timeBased().everyDays(1).create();
}
Reference
Clock Triggers
JS filter
JS map

Google Apps Script creates sheets version of excel file. Issue with multiple creation of versions.

I found a solution for my original question in another post Google Apps Script creates sheets version of excel file.
Testing with the code provided in the answer I ran into another issue. Every time I run the script it creates the Spreadsheets version of the .xlsx files again even if they already exist. I have tried modifying the code withing the last If with no results. Then went back to run your code as posted in case I have missed something but it keeps creating versions of the same files.
Any idea of what could I do to fix this will be really appreciated.
The code provided int he answer is the following.
// Convert the user's stored excel files to google spreadsheets based on the specified directories.
// There are quota limits on the maximum conversions per day: consumer #gmail = 250.
function convertExcelToGoogleSheets()
{
var user = Session.getActiveUser(); // Used for ownership testing.
var origin = DriveApp.getFolderById("origin folder id");
var dest = DriveApp.getFolderById("destination folder id");
// Index the filenames of owned Google Sheets files as object keys (which are hashed).
// This avoids needing to search and do multiple string comparisons.
// It takes around 100-200 ms per iteration to advance the iterator, check if the file
// should be cached, and insert the key-value pair. Depending on the magnitude of
// the task, this may need to be done separately, and loaded from a storage device instead.
// Note that there are quota limits on queries per second - 1000 per 100 sec:
// If the sequence is too large and the loop too fast, Utilities.sleep() usage will be needed.
var gsi = dest.getFilesByType(MimeType.GOOGLE_SHEETS), gsNames = {};
while (gsi.hasNext())
{
var file = gsi.next();
if(file.getOwner().getEmail() == user.getEmail())
gsNames[file.getName()] = true;
}
// Find and convert any unconverted .xls, .xlsx files in the given directories.
var exceltypes = [MimeType.MICROSOFT_EXCEL, MimeType.MICROSOFT_EXCEL_LEGACY];
for(var mt = 0; mt < exceltypes.length; ++mt)
{
var efi = origin.getFilesByType(exceltypes[mt]);
while (efi.hasNext())
{
var file = efi.next();
// Perform conversions only for owned files that don't have owned gs equivalents.
// If an excel file does not have gs file with the same name, gsNames[ ... ] will be undefined, and !undefined -> true
// If an excel file does have a gs file with the same name, gsNames[ ... ] will be true, and !true -> false
if(file.getOwner().getEmail() == user.getEmail() && !gsNames[file.getName()])
{
Drive.Files.insert(
{title: file.getName(), parents: [{"id": dest.getId()}]},
file.getBlob(),
{convert: true}
);
// Do not convert any more spreadsheets with this same name.
gsNames[file.getName()] = true;
}
}
}
}
You want to convert Excel files in origin folder to Google Spreadsheet and put the converted Spreadsheet to dest folder.
When the filename of converted file is existing in dest folder, you don't want to convert it.
If my understanding is correct, how about this modification?
From:
if(file.getOwner().getEmail() == user.getEmail() && !gsNames[file.getName()])
To:
if(file.getOwner().getEmail() == user.getEmail() && !gsNames[file.getName().split(".")[0]])
Note:
In this modification, when the filename of converted file is found in the dest folder, the file is not converted.
When the filename has the extension like ###.xlsx and it is converted to Google Spreadsheet, it seems that the extension is automatically removed. I think that this is the reason that the duplicated files are created. So I used split(".")[0] for this situation.
Reference:
split()

How do I search Google Spreadsheets?

I am doing a few exhaustive searches and need to determine if a new domain (URL) is already in a Spreadsheet. However, none of the Spreadsheet objects have search functions, namely findText() found in most Document objects. I feel like I am missing something significant.
What am I missing?
findText function: https://developers.google.com/apps-script/class_table#findText
SearchResult object: https://developers.google.com/apps-script/class_searchresult
Spreadsheet object: https://developers.google.com/apps-script/class_sheet
My best guess is to try and convert specific Spreadsheet ranges in Document tables, then perform the search. Mendokusai
Unfortunately there is no searching functionality in the Spreadsheet services. You can get the data for the range you are searching on, and then iterate over it looking for a match. Here's a simple function that does that:
/**
* Finds a value within a given range.
* #param value The value to find.
* #param range The range to search in.
* #return A range pointing to the first cell containing the value,
* or null if not found.
*/
function find(value, range) {
var data = range.getValues();
for (var i = 0; i < data.length; i++) {
for (var j = 0; j < data[i].length; j++) {
if (data[i][j] == value) {
return range.getCell(i + 1, j + 1);
}
}
}
return null;
}
I wrote a search tool with a graphical user interface that performs a global search in 3 columns of a single sheet. It could be easily modified to suit your needs. I guess it would be a good idea to add an anchor in the UI to let you open the url you just found.
Here is the code, hoping it will help you to design your own version.
EDIT : I added the anchor widget in the code below (getting its ref in column E)
// G. Variables
var sh = SpreadsheetApp.getActiveSheet();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var lastrow = ss.getLastRow();
//
function onOpen() {
var menuEntries = [ {name: "Search GUI", functionName: "searchUI"},
];
ss.addMenu("Search Utilities",menuEntries);// custom menu
}
// Build a simple UI to enter search item and show results + activate result's row
function searchUI() {
var app = UiApp.createApplication().setHeight(130).setWidth(400);
app.setTitle("Search by name / lastname / adress");
var panel = app.createVerticalPanel();
var txtBox = app.createTextBox().setFocus(true);
var label=app.createLabel(" Item to search for :")
panel.add(label);
txtBox.setId("item").setName("item");
var label0=app.createLabel("Row").setWidth("40");
var label1=app.createLabel("Name").setWidth("120");
var label2=app.createLabel("Lastname").setWidth("120");
var label3=app.createLabel("Street").setWidth("120");
var hpanel = app.createHorizontalPanel();
hpanel.add(label0).add(label1).add(label2).add(label3)
//
var txt0=app.createTextBox().setId("lab0").setName("0").setWidth("40");
var txt1=app.createTextBox().setId("lab1").setName("txt1").setWidth("120");
var txt2=app.createTextBox().setId("lab2").setName("txt2").setWidth("120");
var txt3=app.createTextBox().setId("lab3").setName("txt3").setWidth("120");
var hpanel2 = app.createHorizontalPanel();
hpanel2.add(txt0).add(txt1).add(txt2).add(txt3)
var hidden = app.createHidden().setName("hidden").setId("hidden");
var subbtn = app.createButton("next ?").setId("next").setWidth("250");
var link = app.createAnchor('', '').setId('link');
panel.add(txtBox);
panel.add(subbtn);
panel.add(hidden);
panel.add(hpanel);
panel.add(hpanel2);
panel.add(link);
var keyHandler = app.createServerHandler("click");
txtBox.addKeyUpHandler(keyHandler)
keyHandler.addCallbackElement(panel);
//
var submitHandler = app.createServerHandler("next");
subbtn.addClickHandler(submitHandler);
submitHandler.addCallbackElement(panel);
//
app.add(panel);
ss.show(app);
}
//
function click(e){
var row=ss.getActiveRange().getRowIndex();
var app = UiApp.getActiveApplication();
var txtBox = app.getElementById("item");
var subbtn = app.getElementById("next").setText("next ?")
var txt0=app.getElementById("lab0").setText('--');
var txt1=app.getElementById("lab1").setText('no match').setStyleAttribute("background", "white");// default value to start with
var txt2=app.getElementById("lab2").setText('');
var txt3=app.getElementById("lab3").setText('');
var link=app.getElementById('link').setText('').setHref('')
var item=e.parameter.item.toLowerCase(); // item to search for
var hidden=app.getElementById("hidden")
var data = sh.getRange(2,2,lastrow,4).getValues();// get the 4 columns of data
for(nn=0;nn<data.length;++nn){ ;// iterate trough
if(data[nn].toString().toLowerCase().match(item.toString())==item.toString()&&item!=''){;// if a match is found in one of the 4 fields, break the loop and show results
txt0.setText(nn+2);
txt1.setText(data[nn][0]).setStyleAttribute("background", "cyan");
txt2.setText(data[nn][1]);
txt3.setText(data[nn][2]);
link.setText(data[nn][3]).setHref(data[nn][3]);
sh.getRange(nn+2,2).activate();
subbtn.setText("found '"+item+"' in row "+Number(nn+2)+", next ?");
hidden.setValue(nn.toString())
break
}
}
return app ;// update UI
}
function next(e){
var row=ss.getActiveRange().getRowIndex();
var app = UiApp.getActiveApplication();
var txtBox = app.getElementById("item");
var subbtn = app.getElementById("next").setText("no other match")
var hidden=app.getElementById("hidden");
var start=Number(e.parameter.hidden)+1;//returns the last search index stored in the UI
var item=e.parameter.item.toLowerCase(); // item to search for
var txt0=app.getElementById("lab0");
var txt1=app.getElementById("lab1").setStyleAttribute("background", "yellow");
var txt2=app.getElementById("lab2");
var txt3=app.getElementById("lab3");
var link=app.getElementById('link').setText('').setHref('')
var data = sh.getRange(2,2,lastrow,4).getValues();// get the 4 columns of data
for(nn=start;nn<data.length;++nn){ ;// iterate trough
if(data[nn].toString().toLowerCase().match(item.toString())==item.toString()&&item!=''){;// if a match is found in one of the 4 fields, break the loop and show results
txt0.setText(nn+2);
txt1.setText(data[nn][0]).setStyleAttribute("background", "cyan");
txt2.setText(data[nn][1]);
txt3.setText(data[nn][2]);
link.setText(data[nn][3]).setHref(data[nn][3])
sh.getRange(nn+2,2).activate();
subbtn.setText("found '"+item+"' in row "+Number(nn+2)+", next ?");
hidden.setValue(nn.toString())
break
}
}
return app ;// update UI
}// eof 05-12 Serge insas
I ended up using spreadsheet formulas to solve my problem instead. Specifically, I used the MATCH() function, which can look up a string in an array (in this case a column in another sheet in the same document).
This is significantly simpler than looping through an array, though less efficient and does not allow for full automation. In fact, when the column reached 2,000 entries, Google Drive froze so often, I had to start using Excel instead. Nevertheless, the Match() solution was more appropriate for what I was looking for.
Appreciate all the other responses though.
You can "search" using the SpreadsheetAPI List Feed query parameter. This will return any row that matches using full word matching. Throw some asterisks around your parameter (URL encoded of course) and it becomes wildcard.
I have not tried it, but it appears that the Google Visualization API Query Language will allow you to execute SQL queries against Google sheets.

Resources