Recursively Traverse Object till last leaf node JavaScript - javascript

I am working with Lucene Syntax (a AND b, c OR d) as a search query and I need to translate this search query. In order to translate the Lucene Syntax to a JavaScript object, I use Lucene Parser npm module (https://www.npmjs.com/package/lucene).
for an AND query my translation needs to happen like this:
Query: Hyatt AND Westin
Translated Lucene object:
{
"left":{
"field":"<implicit>",
"fieldLocation":null,
"term":"Hyatt",
"quoted":false,
"regex":false,
"termLocation":{
"start":{
"offset":0,
"line":1,
"column":1
},
"end":{
"offset":6,
"line":1,
"column":7
}
},
"similarity":null,
"boost":null,
"prefix":null
},
"operator":"AND",
"right":{
"field":"<implicit>",
"fieldLocation":null,
"term":"Westin",
"quoted":false,
"regex":false,
"termLocation":{
"start":{
"offset":10,
"line":1,
"column":11
},
"end":{
"offset":16,
"line":1,
"column":17
}
},
"similarity":null,
"boost":null,
"prefix":null
}
}
Translated Search Query for AND:
For an AND query, I get the below translation:
[
[
{
"col":"*",
"test":"Contains",
"value":"Hyatt"
},
{
"col":"*",
"test":"Contains",
"value":"Westin"
}
]
]
Translated Search Query for OR:
For an OR query, I get the below translation:
[
[
{
"col":"*",
"test":"Contains",
"value":"Hyatt"
}
],
[
{
"col":"*",
"test":"Contains",
"value":"Westin"
}
]
]
For this translation I have written the function (readExpression) which recursively traverses through the input(Translated Lucene Object):
result = [];
isFieldImplicit(node: Node): boolean {
return node.field === '<implicit>';
}
isFreeTextSearch(searchText: string): boolean {
// user RegExp for pattern matching of field.f1:abc pattern... i.e. alpha-num.alpha-num:any-char
return !this.isContainingLuceneReservedCharacters(searchText);
}
isAndOperator(expression): boolean {
return expression && expression.operator && expression.operator === 'AND';
}
isOrOperator(expression): boolean {
return expression && expression.operator && expression.operator === 'OR';
}
isQueryContainingQuotes(searchText: string): boolean {
const matches = searchText.match(/"/g);
const firstLastCharacterQuoteMatch = new RegExp(
/((?<![\\])['"])((?:.(?!(?<![\\])\1))*.?)\1/
);
const isFirstAndLastCharacterQuote = firstLastCharacterQuoteMatch.test(
searchText
);
if (!matches) {
return false;
}
if (/""/.test(searchText) && isFirstAndLastCharacterQuote) {
return true;
}
return isFirstAndLastCharacterQuote;
}
isQueryEndingWithStar(searchText: string): boolean {
return searchText.endsWith('*');
}
doesQueryContainsStar(searchText: string): boolean {
return searchText.indexOf('*') !== -1;
}
getTestFromQuery(keyword: string): string {
if (this.isQueryContainingQuotes(keyword)) {
return WHERE_CLAUSE_TESTS.CONTAINS;
} else if (this.isQueryEndingWithStar(keyword)) {
return WHERE_CLAUSE_TESTS.CONTAINS_ANY;
} else {
return WHERE_CLAUSE_TESTS.CONTAINS_ANY;
}
}
generateFreeTextClause(node: Node): WhereClause[] {
const searchText = node.quoted ? `"${node.term}"` : node.term;
const clause = [
{
col: '*',
test: this.getTestFromQuery(searchText as string),
value: (node.term as string).replace('*', '')
} as WhereClause
];
return clause;
}
generateFieldSearchClause(node: Node): WhereClause[] {
const searchText = node.quoted ? `"${node.term}"` : node.term;
const clause = [
{
col: node.field.split('.')[1],
test: this.getTestFromQuery(searchText as string),
value: (node.term as string).replace('*', '')
} as WhereClause
];
return clause;
}
readExpression(expression): any {
let left, right;
if (expression && expression.field) {
return expression;
}
if (this.isOrOperator(expression)) {
left = this.readExpression(expression.left);
right = this.readExpression(expression.right);
if (left) {
if (this.isFieldImplicit(left)) {
this.results.push(this.generateFreeTextClause(left));
} else {
this.results.push(this.generateFieldSearchClause(left));
}
}
if (right) {
if (this.isFieldImplicit(right)) {
this.results.push(this.generateFreeTextClause(right));
} else {
this.results.push(this.generateFieldSearchClause(right));
}
}
console.log(this.results);
return this.results;
}
if (this.isAndOperator(expression)) {
left = this.readExpression(expression.left);
right = this.readExpression(expression.right);
if (left) {
if (this.isFieldImplicit(left)) {
this.results.push(this.generateFreeTextClause(left)[0]);
} else {
this.results.push(this.generateFieldSearchClause(left)[0]);
}
}
if (right) {
if (this.isFieldImplicit(right)) {
this.results.push(this.generateFreeTextClause(right)[0]);
} else {
this.results.push(this.generateFieldSearchClause(right)[0]);
}
}
console.log(this.results);
return this.results;
}
}
It works fine for 1 level AND and OR but as soon as I have 2 level it fails.
Query: (Hyatt AND Westin) OR Orchid
I am expecting below translation:
[
[
{
"col":"*",
"test":"Contains",
"value":"Hyatt"
},
{
"col":"*",
"test":"Contains",
"value":"Westin"
}
],
[
{
"col":"*",
"test":"Contains",
"value":"Orchid"
}
]
]
In the recursive call, the leaf node of OR is getting ignored and not injected inside the array of results. Any suggestions are welcome.

Without trying to dig deeply into your code (see my comments on the question), the core of this should involve a straightforward recursion.
Here I get your output from what I think would be the relevant input:
const convert = (query) =>
'operator' in query
? [convert (query .left), convert (query .right)]
: {
col: '*',
test: 'Contains',
value: query .term
}
const query = {left: {left: {field: "<implicit>", fieldLocation: null, term: "Hyatt", quoted: false, regex: false, termLocation: {start: {offset: 2, line: 1, column: 3}, end: {offset: 8, line: 1, column: 9}}, similarity: null, boost: null, prefix: null}, operator: "AND", right: {field: "<implicit>", fieldLocation: null, term: "Westin", quoted: false, regex: false, termLocation: {start: {offset: 12, line: 1, column: 13}, end: {offset: 18, line: 1, column: 19}}, similarity: null, boost: null, prefix: null}, parenthesized: true}, operator: "OR", right: {field: "<implicit>", fieldLocation: null, term: "Orchid", quoted: false, regex: false, termLocation: {start: {offset: 23, line: 1, column: 24}, end: {offset: 29, line: 1, column: 30}}, similarity: null, boost: null, prefix: null}}
console .log (convert (query))
.as-console-wrapper {max-height: 100% !important; top: 0}
Of course I hard-code col: '*' and test: 'Contains'; you'd have to fill them in appropriately. And my test to see if we're at a branch or leaf ('operator' in query) is likely much too naïve. And you probably will need other cases than what I have here (AND/OR pair and leaf node.) In the end, you might replace every bit of this function, but it could well serve as a skeleton on which to grow the other requirements.
I still think your output format is odd. This result won't distinguish between '(Hyatt AND Westin) OR Orchid', '(Hyatt AND Westin) AND Orchid', '(Hyatt OR Westin) OR Orchid', and '(Hyatt OR Westin) AND Orchid'.

Related

checking array of objects

I wanted to check if each length of array of objects of inspectionScheduleUnitContactArtifactDto , if there is one inspectionScheduleUnitContactArtifactDto which length is equal to 0 return true , if each length of inspectionScheduleUnitContactArtifactDto not 0 or there is no 0 length from inspectionScheduleUnitContactArtifactDto return false;
#sample object
{
"id": 218,
"propertyId": 22977,
"inspectionScheduleUnitArtifactDto": [
{
"id": 524,
"inspectionScheduleUnitContactArtifactDto": [
{
"id": 1097,
"inspectionScheduleUnitArtifactId": 524,
"primaryContactName": "fsdfsdf",
}
],
},
{
"id": 525,
"inspectionScheduleUnitContactArtifactDto": [
{
"id": 1100,
"inspectionScheduleUnitArtifactId": 525,
"primaryContactName": "Name",
},
{
"id": 1101,
"inspectionScheduleUnitArtifactId": 525,
"primaryContactName": "342423432",
}
],
},
{
"inspectionScheduleUnitContactArtifactDto": [],
}
]
}
I believe your question can be simplified to the following:
If at least one object's inspectionScheduleUnitContactArtifactDto array is empty -> return true else return false.
The simplest way to do this is with some which will return true as soon as this condition is met rather than filtering through the entire array. If the condition is not met after searching through the array, then it will return false.
I used the following interfaces to make working with your object structure a bit easier:
export interface InspectionSchedule {
id: number,
propertyId: number,
inspectionScheduleUnitArtifactDto: InspectionScheduleUnitArtifactDto[]
}
export interface InspectionScheduleUnitArtifactDto {
id: number,
inspectionScheduleUnitContactArtifactDto: InspectionScheduleUnitContactArtifactDto[]
}
export interface InspectionScheduleUnitContactArtifactDto {
id: number,
inspectionScheduleUnitArtifactId: number,
primaryContactName: string
}
Importing this interface, you can use the following function:
containsZeroLengthArray(inspectionSchedule: InspectionSchedule) {
return inspectionSchedule.inspectionScheduleUnitArtifactDto
.some(contact => !contact.inspectionScheduleUnitContactArtifactDto.length);
}
Note: !someArray.length is true for an empty array.
I'm going to assume that you know the layout of your object and that you have classes that contain said data. I've created these sample classes to mock the data you showed here:
export class A {
constructor(id: number,
inspectionScheduleUnitArtifactId: number,
primaryContactName: string) { }
}
export class B {
constructor(id: number | null,
inspectionScheduleUnitContactArtifactDto: A[]) { }
}
export class C {
constructor(id: number, propertyId: number,
inspectionScheduleUnitArtifactDto: B[]) { }
}
You can use filter and check the length of the resulting array, like so:
// Setup mock data
const a1: A = new A(1097, 524, 'fsdfsdf');
const a2: A = new A(1100, 525, 'Name');
const a3: A = new A(1101, 525, '342423432');
const b1: B = new B(524, [a1]);
const b2: B = new B(525, [a2, a3]);
const b3: B = new B(null, []);
const c: C = new C(218, 22977, [b1, b2, b3]);
// Check if any of the inner inspectionScheduleUnitContactArtifactDtos are empty
const anyEmpty: boolean = c.inspectionScheduleUnitArtifactDto
.filter(x => x.inspectionScheduleUnitContactArtifactDto.length === 0)
.length > 0;
You can use a recursive function, and pass whatever key you want to determine if an empty property that has the same name exists in your complex objects:
var obj = {
"id": 218,
"propertyId": 22977,
"inspectionScheduleUnitArtifactDto": [
{
"id": 524,
"inspectionScheduleUnitContactArtifactDto": [
{
"id": 1097,
"inspectionScheduleUnitArtifactId": 524,
"primaryContactName": "fsdfsdf",
}
],
},
{
"id": 525,
"inspectionScheduleUnitContactArtifactDto": [
{
"id": 1100,
"inspectionScheduleUnitArtifactId": 525,
"primaryContactName": "Name",
},
{
"id": 1101,
"inspectionScheduleUnitArtifactId": 525,
"primaryContactName": "342423432",
}
],
},
{
"inspectionScheduleUnitContactArtifactDto": [],
}
]
}
function getEmptyProp(obj, key) {
for (var i in obj) {
if (!obj.hasOwnProperty(i)) continue;
if ( obj[i].constructor === Array || obj[i].constructor === Object ) {
if (i == key) {
if (obj[i].length) {
if (getEmptyProp(obj[i], key)) return true;
} else {
return true;
}
} else {
if (getEmptyProp(obj[i], key)) return true;
}
} else {
if (i == key) return true;
}
}
return false;
}
// will return 'true' in this case
console.log(getEmptyProp(obj, 'inspectionScheduleUnitContactArtifactDto'));

Convert one object in another object in TypeScript

I have an database result as shows in file:
So I have input data as follows:
const input = [
{
PurchaseInvoice_id: '8e54a096-568b-48d9-8461-826be53a32da',
PurchaseInvoicePosition_id: '44edfd7f-bc9e-4155-ad5c-5dace9c7c31a',
ProductStock_id: '0a701dbc-2661-4d67-b764-632cfb67334f',
},
{
PurchaseInvoice_id: '8e54a096-568b-48d9-8461-826be53a32da',
PurchaseInvoicePosition_id: '44edfd7f-bc9e-4155-ad5c-5dace9c7c31a',
ProductStock_id: '15278807-794a-4727-9bcb-f7f68dfb4d41',
},
{
PurchaseInvoice_id: '8e54a096-568b-48d9-8461-826be53a32da',
PurchaseInvoicePosition_id: '44edfd7f-bc9e-4155-ad5c-5dace9c7c31a',
ProductStock_id: '0ac9fcd7-73f0-47b1-8fbc-3948863e7a89',
},
{
PurchaseInvoice_id: '8e54a096-568b-48d9-8461-826be53a32da',
PurchaseInvoicePosition_id: '65e013a7-c7b2-47cf-88b7-2ab2d9bcd191',
ProductStock_id: null,
},
{
PurchaseInvoice_id: '8e54a096-568b-48d9-8461-826be53a32da',
PurchaseInvoicePosition_id: '8f00dde6-2548-46a7-a480-37e86a3ca895',
ProductStock_id: '1439dde4-d184-4c98-b0c4-6d3c88ce8496',
},
{
PurchaseInvoice_id: '8e54a096-568b-48d9-8461-826be53a32da',
PurchaseInvoicePosition_id: 'b48711b3-14b1-41ce-9f5f-4032297c1b8e',
ProductStock_id: null,
},
{
PurchaseInvoice_id: '8e54a096-568b-48d9-8461-826be53a32da',
PurchaseInvoicePosition_id: '4e22378d-cf56-4806-bea2-5ba0b220d3eb',
ProductStock_id: null,
},
];
I'd like to convert input object into output object as follows:
const output = [
{
PurchaseInvoice_id: '8e54a096-568b-48d9-8461-826be53a32da',
PurchaseInvoicePosition_ids: [
{
PurchaseInvoicePosition_id: '44edfd7f-bc9e-4155-ad5c-5dace9c7c31a',
ProductStock_ids: [
{
ProductStock_id: '0a701dbc-2661-4d67-b764-632cfb67334f',
},
{
ProductStock_id: '15278807-794a-4727-9bcb-f7f68dfb4d41',
},
{
ProductStock_id: '0ac9fcd7-73f0-47b1-8fbc-3948863e7a89',
},
],
},
{
PurchaseInvoicePosition_id: '65e013a7-c7b2-47cf-88b7-2ab2d9bcd191',
ProductStock_ids: [
{
ProductStock_id: null,
},
],
},
{
PurchaseInvoicePosition_id: '8f00dde6-2548-46a7-a480-37e86a3ca895',
ProductStock_ids: [
{
ProductStock_id: '1439dde4-d184-4c98-b0c4-6d3c88ce8496',
},
],
},
{
PurchaseInvoicePosition_id: 'b48711b3-14b1-41ce-9f5f-4032297c1b8e',
ProductStock_ids: [
{
ProductStock_id: null,
},
],
},
{
PurchaseInvoicePosition_id: '4e22378d-cf56-4806-bea2-5ba0b220d3eb',
ProductStock_ids: [
{
ProductStock_id: null,
},
],
},
],
},
];
Which should looks like on this image when folded:
And I don't know how to convert this way. I'm rather PHP developer, so doing this in JavaScript is hard for me.
I tried do this using some ways like three time iterate over this input object, searching uuid in arrays with no luck.
This input object can have multiple PurchaseInvoice_id which are connected with PurchaseInvoicePosition_id and with ProductStock_id.
Please find the Array.reduce implementation of your question
Logic
Loop through the input array.
Check if there is already a node with the current PurchaseInvoice_id present in the accumulator
If its not present, push the current node to accumulator in the desired pattern (Check Section 1 in the comment added in code)
If its present (Section 2 in code comment), find whether there is a node with current PurchaseInvoicePosition_id in the PurchaseInvoicePosition_ids of the found node
If that is not found (Section 3 in code comment), push your current PurchaseInvoicePosition_id and ProductStock_id to your accumulator.
If that is found (Section 4 in code comment) push your ProductStock_id to the ProductStock_ids of the found node.
Working Fiddle
const input = [
{ PurchaseInvoice_id: '8e54a096-568b-48d9-8461-826be53a32da', PurchaseInvoicePosition_id: '44edfd7f-bc9e-4155-ad5c-5dace9c7c31a', ProductStock_id: '0a701dbc-2661-4d67-b764-632cfb67334f', },
{ PurchaseInvoice_id: '8e54a096-568b-48d9-8461-826be53a32da', PurchaseInvoicePosition_id: '44edfd7f-bc9e-4155-ad5c-5dace9c7c31a', ProductStock_id: '15278807-794a-4727-9bcb-f7f68dfb4d41', },
{ PurchaseInvoice_id: '8e54a096-568b-48d9-8461-826be53a32da', PurchaseInvoicePosition_id: '44edfd7f-bc9e-4155-ad5c-5dace9c7c31a', ProductStock_id: '0ac9fcd7-73f0-47b1-8fbc-3948863e7a89', },
{ PurchaseInvoice_id: '8e54a096-568b-48d9-8461-826be53a32da', PurchaseInvoicePosition_id: '65e013a7-c7b2-47cf-88b7-2ab2d9bcd191', ProductStock_id: null, },
{ PurchaseInvoice_id: '8e54a096-568b-48d9-8461-826be53a32da', PurchaseInvoicePosition_id: '8f00dde6-2548-46a7-a480-37e86a3ca895', ProductStock_id: '1439dde4-d184-4c98-b0c4-6d3c88ce8496', },
{ PurchaseInvoice_id: '8e54a096-568b-48d9-8461-826be53a32da', PurchaseInvoicePosition_id: 'b48711b3-14b1-41ce-9f5f-4032297c1b8e', ProductStock_id: null, },
{ PurchaseInvoice_id: '8e54a096-568b-48d9-8461-826be53a32da', PurchaseInvoicePosition_id: '4e22378d-cf56-4806-bea2-5ba0b220d3eb', ProductStock_id: null, },
];
const output = input.reduce((acc, curr) => {
const purchaseInvoiceNode = acc.find((item) => item.PurchaseInvoice_id === curr.PurchaseInvoice_id);
if (purchaseInvoiceNode) {
// Section 2
const purchaseInvoicePositionNode = purchaseInvoiceNode.PurchaseInvoicePosition_ids.find((item) => item.PurchaseInvoicePosition_id === curr.PurchaseInvoicePosition_id);
if(purchaseInvoicePositionNode) {
// Section 4
purchaseInvoicePositionNode.ProductStock_ids.push({
ProductStock_id: curr.ProductStock_id,
})
} else {
// Section 3
purchaseInvoiceNode.PurchaseInvoicePosition_ids.push({
PurchaseInvoicePosition_id: curr.PurchaseInvoicePosition_id,
ProductStock_ids: [
{
ProductStock_id: curr.ProductStock_id
}
]
})
}
} else {
// Section 1
acc.push({
PurchaseInvoice_id: curr.PurchaseInvoice_id,
PurchaseInvoicePosition_ids: [
{
PurchaseInvoicePosition_id: curr.PurchaseInvoicePosition_id,
ProductStock_ids: [
{
ProductStock_id: curr.ProductStock_id
}
]
}
]
})
}
return acc;
}, []);
console.log(output)
I'm going to call the operation you're performing nestedGroups(). If you have an array of objects arr, and an ordered list of keys key1, key2, key3 (etc.) of the elements of arr, then nestedGroups(arr, key1, key2, key3) will return an object whose key will be a pluralized (just adding "s") version of key1, and whose value will be another array of groupings for the next key, and so on. This is hard to describe in words, so I'll just show an example:
const ng = nestedGroups(
[{ a: 1, b: 2 }, { a: 1, b: 3 }, { a: 4, b: 5 }, { a: 4, b: 6 }],
"a", "b"
);
/* const ng: { as: { a: number; bs: { b: number; }[]; }[]; } */
console.log(ng)
/* {
"as": [
{"a": 1, "bs": [{"b": 2}, {"b": 3}]},
{"a": 4, "bs": [{"b": 5}, {"b": 6}]}
]
} */
If we can implement and strongly type nestedGroups(), then your output will look like:
const r = nestedGroups(input,
"PurchaseInvoice_id", "PurchaseInvoicePosition_id", "ProductStock_id");
const output = r.PurchaseInvoice_ids;
/* const output: {
PurchaseInvoice_id: string;
PurchaseInvoicePosition_ids: {
PurchaseInvoicePosition_id: string;
ProductStock_ids: {
ProductStock_id: string | null;
}[];
}[];
}[] */
which you can verify is the type you want.
First let's implement it and worry about the strong types later:
// implementation
function nestedGroups(arr: Record<string, any>[], ...keys: string[]): any {
const [firstKey, ...restKeys] = keys;
if (firstKey === undefined) return {};
const retmap = new Map<any, any>();
arr.forEach(v => {
const val = v[firstKey];
if (!(retmap.has(val))) retmap.set(val, []);
retmap.get(val)!.push(v);
});
return {
[firstKey + "s"]: Array.from(retmap.entries()).map(([k, v]) =>
({ [firstKey]: k, ...nestedGroups(v, ...restKeys) }))
}
}
This is a recursive function which calls nestedGroups() inside itself unless the array of keys is empty. An empty keys array results in an empty return object. Otherwise, we use the first key firstKey to group the values in the array. Here I'm using a Map to store the values, so each time we see a property value at key1 that we've seen before, we push it onto the end of the array in that Map. And finally, the returned object has a single pluralized firstKey property whose value is an array of objects composed of information from the Map and from the recursive subcall to nestedGroups().
As for the typings, you'll end up needing recursive conditional types (to represent the recursion), variadic tuple types (to pull the array of keys apart) and template literal types (to add that "s" in the type system).
Let's define the NestedGroups<T, K> type to be the type of the return value of nestedGroups(arr, ...keys) when arr is of type T[] and keys is of type K:
type NestedGroups<T extends Record<K[number], Primitive>, K extends string[]> =
K extends [infer F, ...infer R] ? F extends string ? { [P in `${F}s`]:
Array<Pick<T, F> & NestedGroups<T, Extract<R, string[]>> extends
infer O ? { [P in keyof O]: O[P] } : never>; } : never : {};
type Primitive = null | undefined | number | bigint | string | boolean | symbol;
We are constraining T to have keys in K and whose values at those keys are Primitive values (which we define as the union of the types of JavaScript primitives) since those are easily comparable as Map keys. (If the different values of, say, PurchaseInvoicePosition_id were objects, then they probably wouldn't compare as equal, and it would be hard to group by them. Note that "x" === "x" is true but {x: 1} === {x: 1} is false).
Anyway, we will use NestedGroups<T, K> as the return value for nestedGroups(). We can keep the original implementation and just declare that nestedGroups is an overloaded function with a single call signature:
// call signature
function nestedGroups<T extends Record<K[number], Primitive>, K extends string[]>
(arr: T[], ...keys: K): NestedGroups<T, K>;
Okay, time to test it on your input:
const r = nestedGroups(input, "PurchaseInvoice_id", "PurchaseInvoicePosition_id", "ProductStock_id")
const output = r.PurchaseInvoice_ids;
/* const output: {
PurchaseInvoice_id: string;
PurchaseInvoicePosition_ids: {
PurchaseInvoicePosition_id: string;
ProductStock_ids: {
ProductStock_id: string | null;
}[];
}[];
}[] */
console.log(output);
/*
[{
"PurchaseInvoice_id": "8e54a096-568b-48d9-8461-826be53a32da",
"PurchaseInvoicePosition_ids": [
{
"PurchaseInvoicePosition_id": "44edfd7f-bc9e-4155-ad5c-5dace9c7c31a",
"ProductStock_ids": [
{
"ProductStock_id": "0a701dbc-2661-4d67-b764-632cfb67334f"
},
{
"ProductStock_id": "15278807-794a-4727-9bcb-f7f68dfb4d41"
},
{
"ProductStock_id": "0ac9fcd7-73f0-47b1-8fbc-3948863e7a89"
}
]
},
{
"PurchaseInvoicePosition_id": "65e013a7-c7b2-47cf-88b7-2ab2d9bcd191",
"ProductStock_ids": [
{
"ProductStock_id": null
}
]
},
{
"PurchaseInvoicePosition_id": "8f00dde6-2548-46a7-a480-37e86a3ca895",
"ProductStock_ids": [
{
"ProductStock_id": "1439dde4-d184-4c98-b0c4-6d3c88ce8496"
}
]
},
{
"PurchaseInvoicePosition_id": "b48711b3-14b1-41ce-9f5f-4032297c1b8e",
"ProductStock_ids": [
{
"ProductStock_id": null
}
]
},
{
"PurchaseInvoicePosition_id": "4e22378d-cf56-4806-bea2-5ba0b220d3eb",
"ProductStock_ids": [
{
"ProductStock_id": null
}
]
}
]
*/
Yes, that's the right type and the right output value.
There you go. The strong typing here might be overkill for your use case, but since you're asking about TypeScript and not just JavaScript, it might be useful to include it. If all you care about is the output value and not really the type, then you can forgo the typing and just leave it something wide like any. It's up to you.
Playground link to code

updating nested objects in for loop

I have the following issue I have a set of JSON rules like so
{
"event": {
"type": "maxrulecount",
"params": {
"maxrulecount": 2
}
},
"conditions": {
"any": [
{
"all": [
{
"fact": "apples",
"value": "20",
"operator": "greaterThanInclusive"
}
]
},
{
"all": [
{
"fact": "bananas",
"value": "100",
"operator": "greaterThanInclusive"
}
]
}
]
}
}
So i obviously convert this to an object but the number value remains a string so I have created a function that will convert any numbers that are strings into numbers like so
checkForNumberValues(rules) {
// allows any number of numbers together or a decimal number
let numberRegex = /^(([0-9]{1,})|([0-9]{1,}\.[0-9]{1,}))$/g;
// yes a loop within a loop but time complexity is no issue here
rules?.conditions?.any?.forEach((condition) => {
condition?.all?.forEach((rule) => {
console.log(rule.value, numberRegex.test(rule.value)); // this is working correctly
if (numberRegex.test(rule.value)) {
rule.value = Number(rule.value);
}
});
});
console.log(rules);
return rules;
}
now i can see that it is correctly identifying numbers and setting the value but when i console the result like so
console.log(checkForNumberValues(rules));
I'ts returning the rules object with the string number values instead of the number values I set..
Do I need to do something special to set nested values??
Below is an example
let rules = {
conditions: {
any: [
{
all: [
{ fact: 'orange', value: '70' },
{ fact: 'apple', value: '10' },
{ fact: 'brocolli', value: '54' },
{ fact: 'kiwi fruit', value: '199' }
]
}
]
}
}
function checkForNumberValues(rules) {
let numberRegex = /^(([0-9]{1,})|([0-9]{1,}\.[0-9]{1,}))$/g;
rules.conditions.any.forEach((condition) => {
condition.all.forEach((rule) => {
if (numberRegex.test(rule.value)) {
rule.value = Number(rule.value);
}
})
});
return rules;
}
console.log(checkForNumberValues(rules));
Any help would be appreciated!
Regexp "remembers" the last index where a match was found when the global flag g is used (-> Why does a RegExp with global flag give wrong results?)
Use parseInt()/Number() and then test for NaN
let rules = {
conditions: {
any: [
{
all: [
{ fact: 'orange', value: '70' },
{ fact: 'apple', value: '10' }
]
}
]
}
}
function checkForNumberValues(rules) {
rules.conditions.any.forEach((condition) => {
condition.all.forEach((rule) => {
const val = parseInt(rule.value);
if (!isNaN(val)) {
rule.value = val;
}
})
});
return rules;
}
console.log(checkForNumberValues(rules));
Use isNaN to check if the string is a number or not.
let rules = {
conditions: {
any: [
{
all: [
{ fact: 'orange', value: '70' },
{ fact: 'apple', value: '10' },
{ fact: 'brocolli', value: '54' },
{ fact: 'kiwi fruit', value: '199' }
]
}
]
}
}
function checkForNumberValues(rules) {
rules.conditions.any.forEach((condition) => {
condition.all.forEach((rule) => {
if (!isNaN(rule.value)) {
rule.value = Number(rule.value);
}
})
});
return rules;
}
console.log(checkForNumberValues(rules));
you can try a different approach
let rules = {
conditions: {
any: [
{
all: [
{ fact: 'orange', value: '70' },
{ fact: 'apple', value: '10' }
]
}
]
}
}
rules.conditions.any.filter(x=>{(x.all).forEach(x=>x.value=parseInt(x.value))})
console.log(rules)

Deep replace a value knowing the value of an attribute within the object we must inject value in

We have a deeply nested structure which varies each time we run the app.
{
some: {
complex: {
unknown: {
structure: {
fields: [
{ name: "group1", other: "data", currentValue: "" },
{ name: "group2", other: "another data", currentValue: "" },
]
}
}
}
}
}
We must inject, in this structure, proper value. We receive for example
{
group1: 'the proper value'
}
And we must replace the value in the proper group to obtain:
{
some: {
complex: {
unknown: {
structure: {
fields: [
{ name: "group1", other: "data", currentValue: "the proper value" },
{ name: "group2", other: "another data", currentValue: "" },
]
}
}
}
}
}
We tried to use lodash mergeWith but since we cannot know where exactly is the value we must inject and we only know the value of of of the key of the object we must inject the value in, we didn't manage to get this working.
Have a recursive function going through the object and mutating it depending on the value of what you seek.
const obj = {
some: {
complex: {
unknown: {
structure: {
fields: [{
name: 'group1',
other: 'data',
currentValue: '',
},
{
name: 'group2',
other: 'another data',
currentValue: '',
},
],
},
},
},
},
};
const toChange = {
group1: 'the proper value',
group2: 'the proper value 2',
};
// Recursive function that go replace
function lookAndReplace(config, ptr) {
// If we deal with an object look at it's keys
if (typeof ptr === 'object') {
Object.keys(ptr).forEach((x) => {
// If the keys is the one we was looking for check the value behind
if (x === config.keyToCheck) {
// We have found one occurence of what we wanted to replace
// replace the value and leave
if (ptr[x] === config.key) {
ptr[config.keyToReplace] = config.value;
}
return;
}
// Go see into the value behind the key for our data
lookAndReplace(config, ptr[x]);
});
}
// If we are dealing with an array, look for the keys
// inside each of the elements
if (ptr instanceof Array) {
ptr.forEach(x => lookAndReplace(config, x));
}
}
// For each group we look for, go and replace
Object.keys(toChange).forEach(x => lookAndReplace({
key: x,
value: toChange[x],
keyToCheck: 'name',
keyToReplace: 'currentValue',
}, obj));
console.log(obj);
/!\ Important this soluce also work with nested arrays
const obj = {
some: {
complex: {
unknown: {
structure: {
// fields is an array of array
fields: [
[{
name: 'group1',
other: 'data',
currentValue: '',
}],
[{
name: 'group2',
other: 'another data',
currentValue: '',
}],
],
},
},
},
},
};
const toChange = {
group1: 'the proper value',
group2: 'the proper value 2',
};
// Recursive function that go replace
function lookAndReplace(config, ptr) {
// If we deal with an object look at it's keys
if (typeof ptr === 'object') {
Object.keys(ptr).forEach((x) => {
// If the keys is the one we was looking for check the value behind
if (x === config.keyToCheck) {
// We have found one occurence of what we wanted to replace
// replace the value and leave
if (ptr[x] === config.key) {
ptr[config.keyToReplace] = config.value;
}
return;
}
// Go see into the value behind the key for our data
lookAndReplace(config, ptr[x]);
});
}
// If we are dealing with an array, look for the keys
// inside each of the elements
if (ptr instanceof Array) {
ptr.forEach(x => lookAndReplace(config, x));
}
}
// For each group we look for, go and replace
Object.keys(toChange).forEach(x => lookAndReplace({
key: x,
value: toChange[x],
keyToCheck: 'name',
keyToReplace: 'currentValue',
}, obj));
console.log(obj);
const obj = {
some: {
complex: {
unknown: {
structure: {
fields: [{
name: "group1",
other: "data",
currentValue: ""
},
{
name: "group2",
other: "another data",
currentValue: ""
},
]
}
}
}
}
};
const toChange = {
group1: 'the proper value',
group2: 'the proper value 2',
};
// Recursive function that go replace
function lookAndReplace({
key,
value,
keyToCheck,
keyToReplace,
}, ptr) {
// If we deal with an object
if (typeof ptr === 'object') {
Object.keys(ptr).forEach((x) => {
if (x === keyToCheck) {
// We have found one
if (ptr[x] === key) {
ptr[keyToReplace] = value;
}
} else {
lookAndReplace({
key,
value,
keyToCheck,
keyToReplace,
}, ptr[x]);
}
});
}
if (ptr instanceof Array) {
ptr.forEach(x => lookAndReplace({
key,
value,
keyToCheck,
keyToReplace,
}, x));
}
}
// For each group we look for, go and replace
Object.keys(toChange).forEach(x => lookAndReplace({
key: x,
value: toChange[x],
keyToCheck: 'name',
keyToReplace: 'currentValue',
}, obj));
console.log(obj);
A solution could be to use a recursive function like this:
object={
some: {
complex: {
unknown: {
structure: {
fields: [
{ name: "group1", other: "data", currentValue: "" },
{ name: "group2", other: "another data", currentValue: "" },
]
}
}
}
}
};
newValue={
group1: 'the proper value'
};
var inserted=false;
function search(data, newData){
if(inserted) return;
for(key in data){
if(data[key]==Object.keys(newData)[0]){
data["currentValue"]=newData[Object.keys(newData)[0]];
inserted=true;
return;
}else
search(data[key], newData);
}
}
search(object, newValue);
console.log(object);
You could do a recursive search and replace...
let theObj = {
some: {
complex: {
unknown: {
structure: {
fields: [
{ name: "group1", other: "data", currentValue: "" },
{ name: "group2", other: "another data", currentValue: "" },
]
}
}
}
}
}
function updateObj(obj, replacement) {
if(Array.isArray(obj)) {
let key = Object.keys(replacement)[0]
let itm = obj.find(i => i.name == key)
itm.data = replacement[key]
} else if(typeof obj == 'object') {
for(let i in obj) {
updateObj(obj[i], replacement)
}
}
}
updateObj(theObj, { group1: 'the proper value' })
console.log(theObj)

Recursively check for condition on tree and stop when found

I have a basic tree data structure like this (forget about the parent properties that are missing):
data = {
name: 'root',
value: 20,
children: [
{
name: 'child_1',
value: 12,
children: [...]
},
{
name: 'child_2',
value: 8,
children: [...]
},
]
}
And I want to write a function that takes in any item, which can be the root, or any of the children, and do some evaluation on it.
Like the following:
public doCheck(item: TreeItem): boolean {
item.children.forEach( (i: TreeItem) => {
return this.doCheck(i);
});
return (item.children.some( (i: TreeItem) => i.value >= 10));
}
However, right now this seems to be traversing the tree properly, but only returns the evaluation (item.children.some( (i: TreeItem) => i.value >= 10)) as if it was called on the root item alone, for which it will never be true.
Where am I going wrong?
You want to get rid of the forEach and instead recurse inside the some.
I'm going to assume value appears on the entries in children. If so:
function doCheck(item) {
// If `children` is optional, you could add
// `item.children &&` just after `return`
return item.children.some(entry => entry.value >= 10 || doCheck(entry));
}
console.log(doCheck(data)); // true or false
var data = {
name: 'root',
data: [],
value: 5,
children: [
{
name: 'child_1',
data: [],
children: [],
value: 10,
},
{
name: 'child_2',
data: [],
children: [],
value: 20
},
]
};
function doCheck(item) {
// If `children` is optional, you could add
// `item.children &&` just after `return`
return item.children.some(entry => entry.value >= 10 || doCheck(entry));
}
console.log(doCheck(data)); // true, since `child_1` has it
You'll need to add back the type annotations, etc., to turn that back into TypeScript.
If you wanted to find the entry, not just check for it, you'd use find instead of some:
function doCheck(item) {
// If `children` is optional, you could add
// `item.children &&` just after `return`
return item.children.find(entry => entry.value >= 10 || doCheck(entry));
}
console.log(doCheck(data)); // undefined, or the child
var data = {
name: 'root',
data: [],
value: 5,
children: [
{
name: 'child_1',
data: [],
children: [],
value: 10,
},
{
name: 'child_2',
data: [],
children: [],
value: 20
},
]
};
function doCheck(item) {
// If `children` is optional, you could add
// `item.children &&` just after `return`
return item.children.find(entry => entry.value >= 10 || doCheck(entry));
}
console.log(doCheck(data).name);// "child_1"
Not clear what exactly you're trying to do, but this part:
item.children.forEach( (i: TreeItem) => {
return this.doCheck(i);
});
Makes little sense, as you're basically not doing anything here.
You're probably looking for this:
public doCheck(item: TreeItem): boolean {
return
item.children.some(i => i.value >= 10)
&&
item.children.some(i => this.doCheck(i));
}
Maybe || instead of &&.
Which can of course be changed to:
public doCheck(item: TreeItem): boolean {
return item.children.some(i => i.value >= 10 && this.doCheck(i));
}

Categories

Resources