GroupBy 'Linq like' in Typescript - linq

I was looking for a 'groupBy' method in Typescript but it still seems not implemented natively.
Like:
const grouping = items.groupBy('propValue');
The grouping object should be a collection of objects like {key:string, values:any[]}

After some research, I found this solution:
https://gist.github.com/guillaumegarcia13/668518119667594fdca150ebefecd194
Anyway, I structured it a little bit more to have a better typing for my purposes.
The following prototype, supports nested properties and a optional callback parameter to compute some operation on the elements.
/**
* GroupBy array extension
*/
interface Grouping<T> {
key: string,
values: Array<T>,
computed: any
}
interface Array<T> {
groupBy(prop: T, opCallBack: (group: Grouping<T>, item: T) => any): Grouping<T>[];
}
// Nested property support
function getVal(obj, prop) {
const props = prop.split('.');
if (props.length === 1) {
return obj[prop];
} else {
return getVal(obj[props[0]], prop.slice(prop.indexOf('.') + 1, prop.length));
}
}
if (!Array.prototype.groupBy) {
// Return an array of 'Grouping' object
Array.prototype.groupBy = function (prop: string, opCallBack: (group: Grouping<any>, item: any) => any = null) {
return this.reduce((data, item) => {
// Get value
const val = getVal(item, prop);
// Search val
if (data.filter(g => g.key === val).length === 0) {
data.push({
key: val,
values: []
});
}
if(opCallBack) {
opCallBack(data.find(g => g.key === val), item);
}
data.find(g => g.key === val).values.push(item);
return data;
}, []);
}
}
/* End */
An example:
var a = [{a: 'aa', b: 45}, {a: 'aa', b: 45}, {a: 'aa', b: 2}, {a: 'cc', b: 4}, {a: 'cc', b: 45.6}, {a: 'bb', b: 1}];
console.log(a.groupBy('a', (group, item) => {
group.computed = group.computed || 0; group.computed += item.b
}));
// Log:
[
{
"key": "aa",
"values": [
{
"a": "aa",
"b": 45
},
{
"a": "aa",
"b": 45
},
{
"a": "aa",
"b": 2
}
],
"computed": 92
},
{
"key": "cc",
"values": [
{
"a": "cc",
"b": 4
},
{
"a": "cc",
"b": 45.6
}
],
"computed": 49.6
},
{
"key": "bb",
"values": [
{
"a": "bb",
"b": 1
}
],
"computed": 1
}
]
I hope it can be useful

This might not be built-in by default, but there exists a project that brings you linq-like functions to typescript. I suggest that you have a look at the linqts project at https://github.com/kutyel/linq.ts in case you want to use other methods known from LINQ.

Related

Combining two observable sources filtering by one first observable property

Having an observable emitting a list of users with the next content:
[
{
"id": 1,
"name": "John",
"status": "Active"
},
{
"id": 2,
"name": "Mary",
"status": "Inactive"
},
{
"id": 3,
"name": "Peter",
"status": "Inactive"
},
{
"id": 4,
"name": "Susan",
"status": "Active"
}
]
And I have another observable returning the extended user data:
{
"id": 1,
"authorizations: 20
}
I use the detail of each user in an specific details page, but I would like to combine part of the detail in the users list and obtain the next result and only filter by the status Active:
[
{
"id": 1,
"name": "John",
"status": "Active",
"authorizations": 20
},
{
"id": 4,
"name": "Susan",
"status": "Active",
"authorizations": 10
}
]
It is possible to use some filtering operator and combine those results without use two subscriptions?
Tried the following code but, would be a better or simplified way to do it?
import { of, Observable, combineLatest } from 'rxjs';
import { filter, map, mergeAll, mergeMap } from 'rxjs/operators';
type State = 'Active' | 'Inactive';
type User = { id: number; name: string; status: State };
type UserDetail = { id: number; authorizations: number };
type UserWithAuthorizations = User & UserDetail
const users: User[] = [
{
"id": 1,
"name": "John",
"status": "Active"
},
{
"id": 2,
"name": "Mary",
"status": "Inactive"
},
{
"id": 3,
"name": "Peter",
"status": "Inactive"
},
{
"id": 4,
"name": "Susan",
"status": "Active"
}
]
const authorizations: UserDetail[] = [
{ id: 1, authorizations: 20 },
{ id: 2, authorizations: 5 },
{ id: 3, authorizations: 30 },
{ id: 4, authorizations: 10 },
];
const getAuthorizationsByUser= (userId: number): Observable<Partial<UserWithAuthorizations>> => {
const users$ = of(users)
const authorizations$ = of(authorizations)
return combineLatest([users$, authorizations$]).pipe(
map(res => {
const user = res[0].find(u => u.id === userId)
const { authorizations } = res[1].find(a => a.id === userId)
return {
...user,
authorizations
}
}))
};
const fetchUsersWithAuthorizations = () => of(users);
fetchUsersWithAuthorizations()
.pipe(
mergeAll<User>(),
filter((user) => user.status === "Active"),
mergeMap((user) => getAuthorizationsByUser(user.id))
)
.subscribe(console.log);
Why not do it all in a single combine latest?
const { of, map, combineLatest } = rxjs;
const users = [
{
"id": 1,
"name": "John",
"status": "Active"
},
{
"id": 2,
"name": "Mary",
"status": "Inactive"
},
{
"id": 3,
"name": "Peter",
"status": "Inactive"
},
{
"id": 4,
"name": "Susan",
"status": "Active"
}
]
const authorizations = [
{ id: 1, authorizations: 20 },
{ id: 2, authorizations: 5 },
{ id: 3, authorizations: 30 },
{ id: 4, authorizations: 10 },
];
const users$ = of(users)
const authorizations$ = of(authorizations)
const activeUsersWithAuthorizations$ = combineLatest([users$, authorizations$]).pipe(
map(([users, authorizations]) =>
users
.filter((user) => user.status === 'Active')
.map((user) => ({
...user,
authorizations: authorizations.find((a) => a.id === user.id)?.authorizations,
}))
)
);
activeUsersWithAuthorizations$.subscribe(activeUsersWithAuthorizations => {
console.log(activeUsersWithAuthorizations);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.8.0/rxjs.umd.min.js" integrity="sha512-v0/YVjBcbjLN6scjmmJN+h86koeB7JhY4/2YeyA5l+rTdtKLv0VbDBNJ32rxJpsaW1QGMd1Z16lsLOSGI38Rbg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

Improve chart update performance in GAS

I have a function that is updating every chart of a sheet and my main issue is that the updateChart() function is really slow (about 50 seconds) :
I already made a script that parallelize the function but due to the 20 triggers per script limitation I can only run my thread twice. So I wanted to know if there was anything that can speed up the update of my charts.
function ModifyVAxisChart()
{
var ss=SpreadsheetApp.getActiveSpreadsheet().getSheetByName("DCN Dashboard Data");
var rangeMax=ss.getRange("O3:O231").getValues();//Range to modify if you add charts
var rangeMinId=ss.getRange("P3:P232").getValues();//Range to modify if you add charts
var i=0;
var nbChart=39;
//Logger.log("range Max ="+rangeMax + "autre="+rangeMinId);
var Vmin=0;
var Vmax=0;
var id=-1;
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("DCN Dashboard");
var chart = sheet.getCharts();
for(i=0;i<nbChart;i++)
{
Vmax=rangeMax[i*6];
Vmin=rangeMinId[i*6];
id=rangeMinId[(i*6)+1];
var delta=(Vmax-Vmin)*0.1;
Logger.log("Vmax="+Vmax+"Vmin="+Vmin+"id="+id+"i="+i);
var currChart = chart[id];
if(currChart.getType()!="COLUMN")
{
Vmin-=delta
Vmax=Number(Vmax)+(delta*1.5)//Number() function to avoid Vmax becoming a string for no reason
}
Logger.log("Vmax="+Vmax+"Vmin="+Vmin+"id="+id+"i="+i);
currChart = currChart.modify()
.setOption('vAxes', {0: {textStyle: {fontSize: 10}, titleTextStyle: {fontSize : 8}, viewWindow: {min: Vmin, max:Vmax}}})//adpative vaxis for AREA and COMBO
.build();
sheet.updateChart(currChart);
}
}
As the App script functions aren't asynchronous, they will wait until the request is complete to keep running the code (and thus to run the rest of the requests). What you could do is to make the requests using the Sheets API [1] in either JavaScript (you would need to serve and html) or using the UrlFetchApp class [2], this way you could initiate the requests without needing to wait for the response from the previous request.
I implemented the fetchAll() method [2] with one request to update a chart and worked successfully, here is the code (you need to put the sheetID):
function uploadChart() {
var data = SpreadsheetApp.getActiveSpreadsheet();
var sheet = data.getSheets()[1];
var chart = sheet.getCharts()[0];
var chartId = chart.getChartId();
var token = ScriptApp.getOAuthToken();
var url = "https://sheets.googleapis.com/v4/spreadsheets/SHEET_ID:batchUpdate";
var chartBody = {
"updateChartSpec": {
"chartId": chartId,
"spec": {
"title": "Model Q1 Sales",
"basicChart": {
"chartType": "BAR",
"legendPosition": "RIGHT_LEGEND",
"axis": [
{
"format": {
"bold": true,
"italic": true,
"fontSize": 24
},
"position": "BOTTOM_AXIS",
"title": "Sales"
},
{
"format": {
"bold": true,
"italic": true,
"fontSize": 24
},
"position": "LEFT_AXIS",
"title": "Model Numbers"
}
],
"domains": [
{
"domain": {
"sourceRange": {
"sources": [
{
"startRowIndex": 0,
"endRowIndex": 6,
"startColumnIndex": 0,
"endColumnIndex": 1
}
]
}
}
}
],
"series": [
{
"series": {
"sourceRange": {
"sources": [
{
"startRowIndex": 0,
"endRowIndex": 6,
"startColumnIndex": 1,
"endColumnIndex": 2
}
]
}
},
"targetAxis": "BOTTOM_AXIS"
},
{
"series": {
"sourceRange": {
"sources": [
{
"startRowIndex": 0,
"endRowIndex": 6,
"startColumnIndex": 2,
"endColumnIndex": 3
}
]
}
},
"targetAxis": "BOTTOM_AXIS"
},
{
"series": {
"sourceRange": {
"sources": [
{
"startRowIndex": 0,
"endRowIndex": 6,
"startColumnIndex": 3,
"endColumnIndex": 4
}
]
}
},
"targetAxis": "BOTTOM_AXIS"
}
],
"headerCount": 1
}
}
}
}
var requestBody = {
'requests': [chartBody]
}
var request1 = {
'url': url,
'headers': {
'Authorization': 'Bearer ' + token,
},
'method' : 'post',
'contentType': 'application/json',
'payload' : JSON.stringify(requestBody),
'muteHttpExceptions': true
};
var requests = [request1]
var response = UrlFetchApp.fetchAll(requests);
Logger.log(response)
}
I used the update chart json from the "edit a chart" example [1].
To add more requests, you can either add more request jsons in the fetchAll() array parameter or add more update chart jsons in the 'requests' array on requestBody.
[1] https://developers.google.com/sheets/api/samples/charts
[2] https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app#fetchAll(Object)

Transforming object data to array for d3plus-react

I have an api that returns data in the following format:
{
"Information Technology": {
"Name": "Information Technology",
"Change": "0.82%"
},
"Consumer Staples": {
"Name": "Consumer Staples",
"Change": "0.19%"
}
}
I want to convert it to the following format inside my d3plus visualizations:
[
{
"Name": "Information Technology",
"Change": "0.82%"
},
{
"Name": "Consumer Staples",
"Change": "0.19%"
}
]
How do I do this. Here's my React component that uses d3plus:
function Chart() {
const methods = {
groupBy: 'Name',
data: 'https://example.com/api/sectors-performance',
size: d => d.Change
};
return <Treemap config={methods} />;
}
There was a small hint in the docs which helped me come up with this solution:
function Chart() {
const methods = {
groupBy: 'id',
data: 'https://example.com/api/sectors-performance',
size: d => d.value
};
const formatter = d =>
Object.keys(d).map(key => ({
id: d[key].Name,
value: numeral(d[key].Change).value()
}));
return <Treemap config={methods} dataFormat={formatter} />;
}
The trick is to send a formatter as a property!

How to concatenate differents values for one keys

I have a trouble for several days and I need help to solve it.
I have an hash with multiple values for the same key :
{"answers":
[
{"id":1,"value":true},
{"id":3,"value":false},
{"id":2,"value":3},
{"id":1,"value":false},
{"id":2,"value":false},
{"id":2,"value":1}
]
}
I want a method to group all the values for one key, as an exemple :
{
"answers": [
{
"id":1, "value": [true, false]
},
{
"id":3, "value": [false]
},
{
"id":2, "value":[3, false, 1]
}
]
}
I've tried with the reduce method, but I cant find a way to link values to keys.
Anyone can help me with that ?
Thanks!
It looks like you want Enumerable#group_by to regroup the array of hashes by the :id key in each hash.
This method takes the answers array and returns a new, transformed answers array:
def transform_answers(answers)
answers
.group_by { |h| h[:id] }
.each_value { |a| a.map! { |h| h[:value] } }
.map { |id, value| { id: id, value: value } }
end
You can use it like this:
hash = {
answers: [
{ id: 1, value: true },
{ id: 1, value: false },
{ id: 2, value: 3 },
{ id: 2, value: false },
{ id: 2, value: 1 },
{ id: 3, value: false }
]
}
transformed_answers = transform_answers(hash[:answers]) # => [{:id=>1, :value=>[true, false]}, {:id=>2, :value=>[3, false, 1]}, {:id=>3, :value=>[false]}]
You can easily take the transformed answers and put them back into a hash resembling the original input:
transformed_hash = { answers: transformed_answers }
hash = {
answers: [
{ id: 1, value: true },
{ id: 1, value: false },
{ id: 2, value: 3 },
{ id: 2, value: false },
{ id: 2, value: 1 },
{ id: 3, value: false }
]
}
def doit(answers)
answers.each_with_object({}) do |g,h|
h.update(g[:id]=>{ id: g[:id], value: [g[:value]] }) do |_,o,n|
{ id: o[:id], value: o[:value]+n[:value] }
end
end.values
end
{ answers: doit(hash[:answers]) }
#=> {:answers=>[
# {:id=>1, :value=>[true, false]},
# {:id=>2, :value=>[3, false, 1]},
# {:id=>3, :value=>[false]}
# ]
# }
This uses the form of Hash#update (aka merge!) that employs a block to determine the values of keys that are present in both hashes being merged. That block is
do |_k,o,n|
{ id: o[:id], value: o[:value]+n[:value] }
end
See the doc for update for definitions of the three block variables, _k, o and n. I've written the first block variable (the common key) _k, rather than k, to signify that it is not used in the block calculation.
Note that before values is executed in doit the method has constructed the following hash.
{1=>{:id=>1, :value=>[true, false]},
2=>{:id=>2, :value=>[3, false, 1]},
3=>{:id=>3, :value=>[false]}}

Check hash element present and return a value

if I have an array of hashes like below(in JSON) and I want to check if an Id exists.
If the Id exists I then want to return the list of Currencies if they exist(may not in the case of Id 19).
How do I do this ?
"MyArray": [
{
"Id": 14,
"Currencies": {
"48": 840,
"410": 840,
"978": 826
}
},
{
"Id": 19
}
]
a = [ { "Id" => 14, "Currencies" => { "48" => 840, "410" => 840, "978" => 826 } }, { "Id" => 19 } ]
h = a.detect {|i| i["Id"] == 14 && i.has_key?("Currencies") }
h["Currencies"].values unless h.nil?
# => [840, 840, 826]

Resources