google sheets query left join one-to-many - google-sheets

I have 2 tables and I am trying to perform a left join using google query language,or any formula that could output the result set.
Table1
Table2
Result set
How can I accomplish that ?
Regards

OK well here is an inner join to start with:
=ArrayFormula(query(iferror(split(flatten(if(transpose(filter(Table2!B2:B,Table2!B2:B<>""))=filter(Table1!A2:A,Table1!A2:A<>""),filter(Table1!A2:A,Table1!A2:A<>"")&"|"&transpose(filter(Table2!A2:A,Table2!A2:A<>"")),)),"|")),"select Col1,Col2 where Col1 is not null label Col1 '',Col2 ''"))
which builds up a 2D array and fills in the positions where the two sets of data match, then flattens it back into a 1D array and splits it back into two columns.
I think you just have to add the non-matching rows to get a left outer join:
=ArrayFormula({query(iferror(split(flatten(if(transpose(filter(Table2!B2:B,Table2!B2:B<>""))=filter(Table1!A2:A,Table1!A2:A<>""),
filter(Table1!A2:A,Table1!A2:A<>"")&"|"&transpose(filter(Table2!A2:A,Table2!A2:A<>"")),)),"|")),"select Col1,Col2 where Col1 is not null label Col1 '',Col2 ''");
filter(Table1!A2:B,isna(vlookup(Table1!A2:A,Table2!B2:B,1,false)))})
Note
This is a special case where the first table just consists of keys (ID), and you want just the key plus the other column from the second table for rows where the IDs match. It would be straightforward to add more columns separated by a pipe symbol (or any other character of choice), but these would have to be hard-coded: I don't know of any way with this approach to automatically include all columns from both tables.
This is in contrast to the answers here which do automatically combine columns from both tables but don't allow for a one-to-many relationship.

I have also seen many solutions that have complicated formulas using VLOOKUP, INDEX, MATCH, etc.
I decided to write a user function to combine tables, or as I refer to it, de-normalize the database. I wrote the function DENORMALIZE() to support INNER, LEFT, RIGHT and FULL joins. By nesting function calls one can join unlimited tables in theory.
DENORMALIZE(range1, range2, primaryKey, foreignKey, [joinType])
Parameters:
range1, the main table as a named range, a1Notation or an array
range2, the related table as a named range, a1Notation or an array
primaryKey, the unique identifier for the main table, columns start with "1"
foreignKey, the key in the related table to join to the main table, columns start with "1"
joinType, type of join, "Inner", "Left", "Right", "Full", optional and defaults to "Inner", case insensitive
Returns: results as a two dimensional array
Result Set Example:
=QUERY(denormalize("Employees","Orders",1,3), "SELECT * WHERE Col2 = 'Davolio' AND Col8=2", FALSE)
EmpID
LastName
FirstName
OrderID
CustomerID
EmpID
OrderDate
ShipperID
1
Davolio
Nancy
10285
63
1
8/20/1996
2
1
Davolio
Nancy
10292
81
1
8/28/1996
2
1
Davolio
Nancy
10304
80
1
9/12/1996
2
Other Examples:
=denormalize("Employees","Orders",1,3)
=denormalize("Employees","Orders",1,3,"full")
=QUERY(denormalize("Employees","Orders",1,3,"left"), "SELECT * ", FALSE)
=QUERY(denormalize("Employees","Orders",1,3), "SELECT * WHERE Col2 = 'Davolio'", FALSE)
=QUERY(denormalize("Employees","Orders",1,3), "SELECT * WHERE Col2 = 'Davolio' AND Col8=2", FALSE)
=denormalize("Orders","OrderDetails",1,2)
// multiple joins
=denormalize("Employees",denormalize("Orders","OrderDetails",1,2),1,3)
=QUERY(denormalize("Employees",denormalize("Orders","OrderDetails",1,2),1,3), "SELECT *", FALSE)
=denormalize(denormalize("Employees","Orders",1,3),"OrderDetails",1,2)
=QUERY(denormalize("Employees",denormalize("Orders","OrderDetails",1,2),1,3), "SELECT *", FALSE)
=QUERY(denormalize(denormalize("Employees","Orders",1,3),"OrderDetails",4,2), "SELECT *", FALSE)
function denormalize(range1, range2, primaryKey, foreignKey, joinType) {
var i = 0;
var j = 0;
var index = -1;
var lFound = false;
var aDenorm = [];
var hashtable = [];
var aRange1 = "";
var aRange2 = "";
joinType = DefaultTo(joinType, "INNER").toUpperCase();
// the 6 lines below are used for debugging
//range1 = "Employees";
//range1 = "Employees!A2:C12";
//range2 = "Orders";
//primaryKey = 1;
//foreignKey = 3;
//joinType = "LEFT";
// Sheets starts numbering columns starting with "1", arrays are zero-based
primaryKey -= 1;
foreignKey -= 1;
// check if range is not an array
if (typeof range1 !== 'object') {
// Determine if range is a1Notation and load data into an array
if (range1.indexOf(":") !== -1) {
aRange1 = ss.getRange(range1).getValues();
} else {
aRange1 = ss.getRangeByName(range1).getValues();
}
} else {
aRange1 = range1;
}
if (typeof range2 !== 'object') {
if (range2.indexOf(":") !== -1) {
aRange2 = ss.getRange(range2).getValues();
} else {
aRange2 = ss.getRangeByName(range2).getValues();
}
} else {
aRange2 = range2;
}
// make similar structured temp arrays with NULL elements
var tArray1 = MakeArray(aRange1[0].length);
var tArray2 = MakeArray(aRange2[0].length);
var lenRange1 = aRange1.length;
var lenRange2 = aRange2.length;
hashtable = getHT(aRange1, lenRange1, primaryKey);
for(i = 0; i < lenRange2; i++) {
index = hashtable.indexOf(aRange2[i][foreignKey]);
if (index !== -1) {
aDenorm.push(aRange1[index].concat(aRange2[i]));
}
}
// add left and full no matches
if (joinType == "LEFT" || joinType == "FULL") {
for(i = 0; i < lenRange1; i++) {
//index = aDenorm.indexOf(aRange1[i][primaryKey]);
index = aScan(aDenorm, aRange1[i][primaryKey], primaryKey)
if (index == -1) {
aDenorm.push(aRange1[i].concat(tArray2));
}
}
}
// add right and full no matches
if (joinType == "RIGHT" || joinType == "FULL") {
for(i = 0; i < lenRange2; i++) {
index = aScan(aDenorm, aRange2[i][foreignKey], primaryKey)
if (index == -1) {
aDenorm.push(tArray1.concat(aRange2[i]));
}
}
}
return aDenorm;
}
function getHT(aRange, lenRange, key){
var aHashtable = [];
var i = 0;
for (i=0; i < lenRange; i++ ) {
//aHashtable.push([aRange[i][key], i]);
aHashtable.push(aRange[i][key]);
}
return aHashtable;
}
function MakeArray(length) {
var i = 0;
var retArray = [];
for (i=0; i < length; i++) {
retArray.push("");
}
return retArray;
}
function DefaultTo(valueToCheck, valueToDefault) {
return typeof valueToCheck === "undefined" ? valueToDefault : valueToCheck;
}
// Search a multi-dimensional array for a value
function aScan(aValues, searchStr, searchCol) {
var retval = -1;
var i = 0;
var aLen = aValues.length;
for (i = 0; i < aLen; i++) {
if (aValues[i][searchCol] == searchStr) {
retval = i;
break;
}
}
return retval;
}
You can make a copy of the google sheet with data and examples here:
https://docs.google.com/spreadsheets/d/1vziuF8gQcsOxTLEtlcU2cgTAYL1eIaaMTAoIrAS7mnE/edit?usp=sharing

use in B2:
=ARRAYFORMULA(IFNA(VLOOKUP(Table1!A2:A, {Table2!B:B, Table2!A:A}, 2, 0)))

Related

How to conditionally copy a row in google sheets to a tab in a loop but not duplicating

Ok, complete newb here...I've searched and found various 'copy row to another sheet' scripts but can only get one to work. Here is my bodged version below. What I am trying to do is have a front sheet that displays the rows from sheet 1 where one of three criteria is met i.e column N has a status of 'Edit', 'SB Edit' or 'Amend Edit'. The below does do this but it doesn't loop and each time I run it, it repeats the action and I get duplicates of all the rows on the destination sheet.
Essentially, I want it running such that any time the status of column N for a particular row is changed to one of these three values, it shows up on Sheet 2. So Sheet 2 is a 'live' display of all rows in sheet 1 where column N equals the specified value
Any help much appreciated...
function copyrange() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Sheet 1'); //source sheet
var testrange = sheet.getRange('N:N');
var testvalue = (testrange.getValues());
var csh = ss.getSheetByName('Sheet 2'); //destination sheet
var data = [];
var j =[];
//Condition check in N:N; If true copy the same row to data array
for (i=0; i<testvalue.length;i++) {
if ( testvalue[i] == 'Edit') {
data.push.apply(data,sheet.getRange(i+1,1,1,15).getValues());
//Copy matched ROW numbers to j
j.push(i);
}
}
//Copy data array to destination sheet
csh.getRange(csh.getLastRow()+1,1,data.length,data[0].length).setValues(data);
for (i=0; i<testvalue.length;i++) {
if ( testvalue[i] == 'Amend Edit') {
data.push.apply(data,sheet.getRange(i+1,1,1,15).getValues());
//Copy matched ROW numbers to j
j.push(i);
}
}
//Copy data array to destination sheet
csh.getRange(csh.getLastRow()+1,1,data.length,data[0].length).setValues(data);
for (i=0; i<testvalue.length;i++) {
if ( testvalue[i] == 'SB Edit') {
data.push.apply(data,sheet.getRange(i+1,1,1,15).getValues());
//Copy matched ROW numbers to j
j.push(i);
}
}
//Copy data array to destination sheet
csh.getRange(csh.getLastRow()+1,1,data.length,data[0].length).setValues(data);
//Delete matched rows in source sheet
//for (i=0;i<j.length;i++){
//var k = j[i]+1;
//sheet.deleteRow(k);
//Alter j to account for deleted rows
//if (!(i == j.length-1)) {
//j[i+1] = j[i+1]-i-1;
}
If you want that copy to happen when the column N is modified, it's probably best to set a trigger, like onEdit. That will give you an object with data about the event that happened, including the cell that was changed and its value.
You can then use such data to get the whole row and copy it to the other sheet, like this:
function onEdit(e) {
const statuses = ['Edit', 'SB Edit', 'Amend Edit'];
// Check if the modification happened on column N (14th column) and if the new value is one of the desired statuses
if (e.range.getColumn() === 14 && statuses.includes(e.value)) {
// Get sheets
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheet1 = spreadsheet.getSheetByName('Sheet1');
const sheet2 = spreadsheet.getSheetByName('Sheet2');
// Get the whole row of the modified cell
const modifiedRow = sheet1.getRange(e.range.getRow(), 1, 1, sheet1.getLastColumn()).getValues();
// Append modified row to the end of Sheet2
sheet2.appendRow(modifiedRow.flat());
}
}
If you want to avoid duplicated rows in Sheet2, then you need to have a way to check whether a particular row was added to Sheet2 before.
For example, if column A has a unique identifier (ID) for rows, you can search for that ID in Sheet2 before appending a new row. If it exists, then you might want to replace it with the new values or just do nothing. If it doesn't exist, then you append the row to the end:
function onEdit(e) {
// Check if the modification happened on column N (14th column) and if the new value is one of the desired statuses
if (e.range.getColumn() === 14) {
// Specify update statuses
const addStatuses = ['Edit', 'SB Edit', 'Amend Edit'];
const removeStatuses = ['On Hold', 'Version Returned'];
// Get sheets
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheet1 = spreadsheet.getSheetByName('Sheet1');
const sheet2 = spreadsheet.getSheetByName('Sheet2');
// Get the whole row of the modified cell
const modifiedRow = sheet1.getRange(e.range.getRow(), 1, 1, sheet1.getLastColumn()).getValues();
// Assume that ID is in column A (first column)
const modifiedRowID = modifiedRow[0][0];
// Get values from Sheet2
const columnValues = sheet2.getDataRange().getValues();
// Check if Sheet2 already has row based on the value of column A and retrieve its index
let rowIndex = -1;
for (let i = 0; i < columnValues.length; i++) {
if (columnValues[i][0] == modifiedRowID) {
rowIndex = i + 1; // JavaScript arrays indices start from 0, but Sheets row indices start from 1
break;
}
}
// If row should be added/removed to/from Sheet2
if (addStatuses.includes(e.value)) {
if (rowIndex == -1) {
// Row not found in Sheet2, so add it
sheet2.appendRow(modifiedRow.flat());
} else {
// Optional: Row found in Sheet2, so replace it with current values
sheet2.getRange(rowIndex, 1, 1, modifiedRow[0].length).setValues(modifiedRow);
}
} else if (removeStatuses.includes(e.value) && rowIndex > -1) {
// Remove row from Sheet2
sheet2.deleteRow(rowIndex);
}
}
}

Data attribute for (Mottie) Tablesorter filter_selectSource

I have a dynamic table which can contain a status column that can contain a predefined list of status, for example:
0: closed
1: Open
2: Pending
3: ...
The status label is displayed in the table, but the id number is used for actual filtering. I successfully applied tablesorter filter-select to display a select filter, but it either display label (won't filter) or id (not pretty).
I could fix this using filter_selectSource inside javascript, but since my table is dynamic and displayed using Handlebar, I'm looking for an html solution using data attributes.
Is there a data attribute that could be used to set the filter select label/value, similar to how data-text can be used to define unparsed text? Or is there a way to define a custom parser for filter that would return a label/value combo as an array for example?
Based on Mottie reply and tablesorter.filter.getOptions source, I came up with this. Adding the filter-metaselect class to my column(s) th enables the data-value attribute in the cell td to be used as the select options. The parsed/unparsed text can still be used. Note that the child part of getOptions has been omitted since I'm not using feature at the moment.
Table Cell :
<td data-value="1">
Projet actif
</td>
Select option :
<option value="1" parsed="projet actif" data-function-name="1">Projet actif</option>
Javascript:
filter_selectSource: {
".filter-metaselect": function (table, column, onlyAvail) {
table = $( table )[0];
var rowIndex, tbodyIndex, len, row, cache, indx, child, childLen, colData,
c = table.config,
wo = c.widgetOptions,
arry = [];
for ( tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) {
cache = c.cache[tbodyIndex];
len = c.cache[tbodyIndex].normalized.length;
// loop through the rows
for ( rowIndex = 0; rowIndex < len; rowIndex++ ) {
// get cached row from cache.row ( old ) or row data object
// ( new; last item in normalized array )
row = cache.row ?
cache.row[ rowIndex ] :
cache.normalized[ rowIndex ][ c.columns ].$row[0];
// check if has class filtered
if ( onlyAvail && row.className.match( wo.filter_filteredRow ) ) {
continue;
}
// Get the column data attributes
if (row.getElementsByTagName('td')[column].getAttribute('data-value')) {
colData = row.getElementsByTagName('td')[column].getAttribute('data-value');
} else {
colData = false;
}
// get non-normalized cell content
if ( wo.filter_useParsedData ||
c.parsers[column].parsed ||
c.$headerIndexed[column].hasClass( 'filter-parsed' ) ) {
arry[ arry.length ] = {
value : (colData) ? colData : cache.normalized[ rowIndex ][ column ],
text : cache.normalized[ rowIndex ][ column ]
};
// child row parsed data
/* TODO */
} else {
arry[ arry.length ] = {
value : (colData) ? colData : cache.normalized[ rowIndex ][ c.columns ].raw[ column ],
text : cache.normalized[ rowIndex ][ c.columns ].raw[ column ]
};
// child row unparsed data
/* TODO */
}
}
}
// Remove duplicates in `arry` since using an array of objects
// won't do it automatically
var arr = {};
for ( var i=0, len=arry.length; i < len; i++ )
arr[arry[i]['text']] = arry[i];
arry = new Array();
for ( var key in arr )
arry.push(arr[key]);
return arry;
}
}
The filter_selectSource documentation has an example where this widget option calls the filter.getOptions which returns an array of cell text or parsed values (based on the filter parser setting); if that doesn't return the values you want, grab the values yourself and return an array in that function.
Here is a basic example of how to use it: http://jsfiddle.net/Mottie/856bzzeL/117/ (related to Is there a way in tablesorter to filter to select only rows where the field is empty?)
$(function(){
$('table').tablesorter({
theme: 'blue',
widgets: ['zebra', 'filter'],
widgetOptions: {
filter_functions: {
0: {
'{empty}' : function (e, n, f, i, $r, c) {
return $.trim(e) === '';
}
}
},
filter_selectSource: {
0: function (table, column, onlyAvail) {
// get an array of all table cell contents for a table column
var array = $.tablesorter.filter.getOptions(table, column, onlyAvail);
// manipulate the array as desired, then return it
array.push('{empty}');
return array;
}
}
}
});
});

ui-grid get the current column (field) name from inside a custom sorting algorithm?

Once you define custom sorting for a column like in Github and UI-Grid
How can you access the column from inside the algorithm?
var myAwesomeSortFn = function(a,b, rowA, rowB, direction){
// "Need to access the name (field) of column being sorted here";
var column = "No Idea"
console.log("sorting by column " + column );
if (a == b) return 0;
if (a < b) return -1;
if (a > b) return 1;
};
You could try the following...
{ field: 'lastName', displayName: 'Last Name', sortingAlgorithm: MyService.getSortingAlgorithm('lastName') },
Then define in a service (or in your scope if you prefer)
getSortingAlgorithm: function (columnName) {
return function(a, b, rowA, rowB, direction) {
console.log("sorting by column " + columnName);
if (a == b) return 0;
if (a < b) return -1;
if (a > b) return 1;
};
}

How to perform SUM operation in Entity Framework

I have a table.
create table tblCartItem(
pkCartItemId int primary key identity,
CartId int not null,
ProductId int not null,
Quantity int not null,
Price nvarchar(15)
)
and I want to perform sum opeartion on that like as
Select SUM(Price) from tblCartItem where CartId='107'
and I am trying to following code but its not working
ObjTempCart.CartTotal = (from c in db.tblCartItems where c.CartId == cartId select c.Price).Sum();
Any one help me to do this using Entity Framework.
I am using MVC 4 Razor.
May be You can use lambda Expression
var total=db.tblCartItems.Where(t=>t.CartId == cartId).Sum(i=>i.Price);
its working try this..
use Decimal.Parse to convert price.
ObjTempCart.CartTotal = db.tblCartItems.Where(t=>t.CartId == cartId).Select(i=>Decimal.Parse(i.Price)).Sum();
Finally I have a solution of that but its not exactly from Entity Framework, But its working...
private double CartItemTotalPrice(Int32 CartID)
{
List<string> pricelst = new List<string>();
pricelst = (from c in db.tblCartItems where c.CartId == CartID select c.Price).ToList();
double Total = 0;
if (pricelst != null)
{
for (int i = 0; i < pricelst.Count; i++)
{
Total += Convert.ToDouble(pricelst[i]);
}
}
return Total;
}
Decimal.parse not working, try Convert.toDouble
double total = _context.Projecao
.Where(p => p.Id == idProj)
.Select(i => Convert.ToDouble(i.ValorTotal)).Sum();

Result of MAX()- method

I have table tb_Orders (it empty), which have fields^
- order_id (int) (primary key)
- order_date nchar(30)
In my application, when client make order, requests the function:
private int GetNewOrderId()
{
int ord_id = 0;
if (db.tb_Orders.Max(x => x.order_id) != null)
{
int ord = db.tb_Orders.Max(x => x.order_id);
ord_id = ord + 1;
}
else
{
ord_id = 1;
};
return ord_id;
}
which get the new order id (+1 to max order in table).
Operator "if" must, when the table is still empty, get id = 1;
But the result - error (when I try to get id).
ERROR TEXT: "Error converting cast a value type "Int32", as materialize value is null."
Try casting your order_id to a nullable integer when making the Max call:
private int GetNewOrderId()
{
int nextOrderId = db.tb_Orders.Max(x => (int?)x.order_id) ?? 1;
return nextOrderId;
}
You will also notice that in my example there's only a single SQL query to the database whereas you were making 2: one in the if statement and another one inside.
It seems your order_id is Nullable<int>. Use the Value property to get it's value, and you can also perform the query before if statement and don't execute the query twice:
var max = db.tb_Orders.Max(x => x.order_id);
if(max != null)
{
int ord = max.Value;
ord_id = ord + 1;
}

Resources