when we need to compare two objects a and b we also should test that one of them is not null.
However, knowing that is a total chaos
{} - null => -0
[] - null => 0
1 - null => 1
"1" - null => 1
true - null => 1
false - null => 0
"a" - null => NaN
null - null => 0
"a" == null false
"a" > null false
"a" < null false
let arr = [
{ name: "a" },
{ name: null },
null,
{ name: "zaa" },
{ name: "dgh" }
];
let sortByName = function (a, b) {
if (a == null || b == null) return a - b;
if (a.name == null || b.name == null) return a.name - b.name;
return a.name.localeCompare(b.name);
};
console.log(arr.sort(sortByName));
the result is the following:
0: {name: 'a'}
1: {name: null}
2: null
3: {name: 'dgh'}
4: {name: 'zaa'}
how would you explain such a result?
null - {} === NaN
{} - null === -0
Here:
if (a == null || b == null) return a - b;
You are subtracting anything from null or null from anything, if one of the 2 values is null.
Replace null with an empty string and use that in your comparison:
let arr = [
{ name: "a" },
{ name: null },
null,
{ name: "zaa" },
{ name: "dgh" }
];
let sortByName = function (a, b) {
return (a?.name ?? '').localeCompare(b?.name ?? '');
};
console.log(arr.sort(sortByName));
If you want to make it so that null comes before {name: null} you can extend #Cerbrus answer to handle this.
A simple way to do this is convert both terms a and b into values that will in the end sort the way you like. I find the easiest way is to just prefix the input terms with another character you want to sort on.
eg.
If term is null, just return `0`;
If term is `{name:null}` return `1`;
Everything else just return `3` + term;
It's kind of like creating a compound index..
Doing the above you can see it's very easy to make the sort do whatever you want based on certain conditions.. Eg, if you wanted null to go to the end like undefined simply change it's prefix, eg. make it 4
eg.
let arr = [
{ name: "a" },
{ name: null },
null,
{ name: "zaa" },
{ name: "dgh" }
];
function toStr(a) {
if (a === null) return '0';
else if (a.name === null) return '1';
else return '3' + a.name;
}
let sortByName = function (a, b) {
return toStr(a).localeCompare(toStr(b));
};
console.log(arr.sort(sortByName));
Related
I am trying to write a find function to find items from matched items from a potentially nested array (without having to flat the array first) and I am trying to write in a FP way.
Here is my attempt:
const nestedArray = [
[{ id: 1 }],
[{ id: 2 }],
[{ id: 3 }, [{ id: 4 }]],
{ id: 5 },
]
function findTarget(arr, predicate) {
const helper = ([x, ...xs]) =>
x === undefined
? null
: predicate(x)
? x
: Array.isArray(x)
? helper(x) ?? helper(xs)
: helper(xs)
return helper(arr)
}
findTarget(nestedArray, (item) => item.id === 5)
I think it works but it is not super readable and I am sure there are better ways to write such a function.
Here's how I would implement this using recursion:
function findTarget(value, predicate) {
const isArray = Array.isArray(value);
// Base case: if value is not array and predicate matches, we found a match
if (!isArray) {
if (predicate(value)) return value;
return null;
}
// value must be an array, so run recursion and see if value exists
for (const item of value) {
const foundItem = findTarget(item, predicate);
if (foundItem !== null) {
return foundItem;
}
}
// nothing found
return null;
}
does the same thing that your code does and imo looks cleaner.
Since your example is calling predicate(x) in the first place, it will return a false positive when matching an array with an id: 5 property, so the Array.isArray(x) should go first to avoid this:
const nestedArray = [
Object.assign([{ id: 1 }], { id: 5 }),
[{ id: 2 }],
[{ id: 3 }, [{ id: 4 }], null, [[{ id: 5 }]]],
{ id: 6 },
]
function findTargetLoop (arr, match) {
if (!Array.isArray(arr))
return arr && match(arr) ? arr : null;
let item, i = 0;
while (!(item = findTargetLoop(arr[i++], match)) && i < arr.length);
return item ?? null;
}
const findTargetFunc = (arr, match, next) =>
(next = ([item, ...rest]) =>
Array.isArray(item) ? next(item) ?? next(rest)
: item && match(item) ? item
: rest.length ? next(rest) : null)(arr);
const match = item => item.id === 5;
console.log('with iterations', findTargetLoop(nestedArray, match));
console.log('pure functional', findTargetFunc(nestedArray, match));
Here's one approach I can think of. It uses the init function as a sentinel value to distinguish whether the element being searched for has already been found. Before returning, it invokes the accumulated value which is either () => undefined, or () => curr capturing the first element that matches the predicate.
const flatFind = (array, predicate) => {
const init = () => undefined
const reducer = (prev, curr) => (
prev === init
? Array.isArray(curr)
? curr.reduce(reducer, init)
: predicate(curr)
? () => curr
: init
: prev
)
return array.reduce(reducer, init)()
}
const nestedArray = [
[{ id: 1 }],
[{ id: 2 }],
[{ id: 3 }, [{ id: 4 }]],
{ id: 5 },
]
console.log(flatFind(nestedArray, item => item.id === 5))
Your form is a good start but the helper is unnecessary and the order of conditions should be changed. Here we use inductive reasoning -
If x is undefined, there is nothing left to match. Return no match.
(inductive) x is defined. If x is an array, find within x or find within xs
(inductive) x is defined and x is a non-array. If x matches predicate f, return the match
(inductive) x is defined and x is a non-array that does not match the predicate f. Find within the sub-problem, xs.
const find = ([x, ...xs], f) =>
x === undefined // 1
? null
: Array.isArray(x) // 2
? find(x, f) ?? find(xs, f)
: f(x) // 3
? x
: find(xs, f) // 4
const t = [
[{ id: 1 }],
[{ id: 2 }],
[{ id: 3 }, [{ id: 4 }]],
{ id: 5 },
]
console.log(find(t, (item) => item?.id === 5)) // { id: 5 }
console.log(find(t, (item) => item?.id === 9)) // null
Note, the behavior of your findTarget checks child arrays against the predicate and allows for return of arrays that match the predicate. This is inconsistent behaviour as it's not possible to match the outermost array and the predicate checks for .id properties on arrays. The find solution above avoids this by changing the order of conditions 2 and 3. If you want the ability to return array matches with your function, you can change the order to 1,3,2,4.
I have ask another question, but someone close that question. I really need this answer. That's why I asking another question.
I have a object like following. I have to remove that empty string filed from nested object and also from nested array. How can I remove that.
const obj = {
name: 'Red Boy',
price: '350',
originalPrice: '', // Empty string field
stock: 20,
category: {
name: '', // Empty String field
subCategory: { name: ''} // Empty String filed
},
weight: '90kg',
dimensions: {
width: '50cm',
height: '', // Empty string filed
length: '70cm'
},
suitable: [
{ name: 'Yoga' },
{ name: '' }, // Empty String filed
{ name: 'Winter' }
],
additionalInfo: [
{ field: 'closure', value: 'Button' },
{ field: 'collar', value: ''} // Empty String Field
]
}
In this hybrid object type you can see some sub-object and also some sub-array. You can also see some field that are not contain any value.(I comment out that filed).
Actually I need to remove that filed. How can I remove that empty string field from above hybrid object type.
Thank you..
My Expected result-
{
name: 'Red Boy',
price: '350',
// Removed
stock: 20,
category: {
name: '', // Empty String field
// Removed
},
weight: '90kg',
dimensions: {
width: '50cm',
// Removed
length: '70cm'
},
suitable: [
{ name: 'Yoga' },
//Removed
{ name: 'Winter' }
],
additionalInfo: [
{ field: 'closure', value: 'Button' },
{ field: 'collar', //Removed }
// Here If this two filed is empty then should remove the whole object
{ field: '', value: '' }
// Then should remove whole '{ field: '', value: '' }'
]
}
To achieve this, we need to implement a recursive function to remove all empty string in all nested arrays and objects.
function rec(obj){
for(let key of Object.keys(obj)){
if (obj[key] === ''){
delete obj[key];
}
else if (typeof obj[key] === 'object'){
obj[key] = rec(obj[key]);
if (Object.keys(obj[key]).length === 0 ) delete obj[key];
}
}
return Array.isArray(obj) ? obj.filter(val => val) : obj;
}
Also, please note that it's not purely hybrid. Because Array is special type of Object.
const obj = {
name: 'Red Boy',
price: '350',
originalPrice: '', // Empty string field
stock: 20,
category: {
name: '', // Empty String field
subCategory: { name: ''} // Empty String filed
},
weight: '90kg',
dimensions: {
width: '50cm',
height: '', // Empty string filed
length: '70cm'
},
suitable: [
{ name: 'Yoga' },
{ name: '' }, // Empty String filed
{ name: 'Winter' }
],
additionalInfo: [
{ field: 'closure', value: 'Button' },
{ field: 'collar', value: ''} // Empty String Field
]
}
function removeEmptyString(object) {
Object
.entries(object)
.forEach(([key, value]) => {
if (value && typeof value === 'object')
removeEmptyString(value);
if (value &&
typeof value === 'object' &&
!Object.keys(value).length ||
value === null ||
value === undefined ||
value.length === 0
) {
if (Array.isArray(object))
object.splice(key, 1);
else
delete object[key];
}
});
return object;
}
console.log(removeEmptyString(obj))
I have used recursion to filter out the empty string, empty object or empty array present deep inside the nested structure.
This function also removes such objects and their nested objects with no properties.
Note: It will also work if the provided initial value is any other thing then object like array or string
var obj={name:"Red Boy",price:"350",originalPrice:"",stock:20,category:{name:"",subCategory:{name:""}},weight:"90kg",dimensions:{width:"50cm",height:"",length:"70cm"},suitable:[{name:"Yoga"},{name:""},{name:"Winter"}],additionalInfo:[{field:"closure",value:"Button"},{field:"collar",value:""}]};
function filt(a) {
if (typeof a === 'string') return a !== '';
//if it is a string, then it must not be empty
else if (Array.isArray(a)) return a.length !== 0
//if it an arra, then it must have some item
else if (a instanceof Object) return Object.keys(a).length !== 0;
//if it is an object, then it must have some property
return a !== null && a !== undefined
//else it must not be null or undefined
}
function rec(obj) {
if (Array.isArray(obj)) {
//if an value is an array
return obj.map((a) => rec(a)).filter((a) => filt(a)) //recurse the child first of each value in the array
//then filter out the value which are either null, empty, undefined or have length 0
} else if (obj instanceof Object) {
//if value is an object
var d = Object.entries(obj).map((a) => ([a[0], rec(a[1])])).filter((a) => filt(a[1]));
//map through the object.entries and reassign the values to the keys by recurssing over the value to filter out the nested inside irrelevant value
return Object.fromEntries(d)
//convert the map into object and return
} else if (typeof obj === 'string') return obj !== '' ? obj : null
//f it is a string, it must not be empty else return null
return obj !== null && obj !== undefined ? obj : null
//else it must not be null or undefined
}
console.log("For object",rec(obj))
console.log("For Array",rec([{
name: "Yoga"
}, {
name: ""
}, {
name: "Winter"
}]))
Here's an immutable way to remove non-empty values from an object, with the added capability of normalising object's inside of arrays:
const normaliseObject = (obj) =>
Object.fromEntries(
Object.entries(obj)
.filter(([_, value]) => value !== '' && value !== null)
.map(([key, value]) => (typeof value === 'object' ? [key, normalise(value)] : [key, value])),
)
// If an element in an array is an object, normalise that too
const normaliseArray = (arr) => arr.map(value => (typeof value === 'object' ? normalise(value) : value))
/**
* Normalise any object to remove keys whose values are falsey
* #param obj Object to be normalised
* #returns Normalised object where falsey values are removed
*/
const normalise = (obj) => {
return Array.isArray(obj) ? normaliseArray(obj) : normaliseObject(obj)
}
I have a model that looks as follows
this.Model
Model {class: undefined items: Array(0) tag: undefined launch: undefined length: undefined name: undefined Id: "de4d704a-b754-4546-b3ab-f0c131eba84a" time: "15:36" tonnage: undefined}
the only objects in the Model that will always have a value is Id and Time.
i have an if statement that goes through each off my objects to check if its null as follows:
if ( this.Model.class == null && this.Model.name == null && this.Model.tag== null && this.Model.launch == null && this.Model.length == null && this.Model.tonnage == null && this.Model.items.length == 0)
{
//does something in here
}
so i want to check if all the objects are null except time and ID, is there a better way of doing this than me using the above method in an if statement?
I would create a function to check about that using Object.entries and Array.every.
Perks of this solution :
Reusable utility function.
Works with any number of keys to ignore.
The typing of the function will throw an error in case you specify a key to ignore that is not a part of the provided object.
Playground in TypeScript
Snippet in Javascript.
function checkAllKeysExceptGivenKeysToBeNullable(obj, keys) {
return Object.entries(obj).every(([
key,
value,
]) => {
// If the key has to be ignored
if (keys.includes(key)) {
return true;
}
// Check the value to be nullable
return value === null ||
value === void 0 ||
(value instanceof Array && value.length === 0);
});
}
console.log(checkAllKeysExceptGivenKeysToBeNullable({
class: undefined,
items: Array(0),
tag: undefined,
launch: undefined,
length: undefined,
name: undefined,
Id: 'de4d704a-b754-4546-b3ab-f0c131eba84a',
time: '15:36',
tonnage: undefined,
}, [
'Id',
'time',
]));
console.log(checkAllKeysExceptGivenKeysToBeNullable({
class: undefined,
items: Array(0),
tag: 'nope',
launch: undefined,
length: undefined,
name: undefined,
Id: 'de4d704a-b754-4546-b3ab-f0c131eba84a',
time: '15:36',
tonnage: undefined,
}, [
'Id',
'time',
]));
function checkAllKeysExceptGivenKeysToBeNullable<T extends {
[key in keyof T]: null | undefined | Array<unknown> | unknown;
}>(obj: T, keys: (keyof T)[]): boolean {
return Object.entries(obj).every(([
key,
value,
]) => {
if (keys.includes(key as keyof T)) {
return true;
}
return value === null ||
value === void 0 ||
(value instanceof Array && value.length === 0);
});
}
console.log(checkAllKeysExceptGivenKeysToBeNullable({
class: undefined,
items: Array(0),
tag: undefined,
launch: undefined,
length: undefined,
name: undefined,
Id: 'de4d704a-b754-4546-b3ab-f0c131eba84a',
time: '15:36',
tonnage: undefined,
}, [
'Id',
'time',
]));
console.log(checkAllKeysExceptGivenKeysToBeNullable({
class: undefined,
items: Array(0),
tag: 'nope',
launch: undefined,
length: undefined,
name: undefined,
Id: 'de4d704a-b754-4546-b3ab-f0c131eba84a',
time: '15:36',
tonnage: undefined,
}, [
'Id',
'time',
]));
After playing around a bit, I came up with a solution using Object.keys()
/**
* Checks if all of the objects values are null, except for keys in param except
* #param obj: The object to test
* #param except (optional): keys to omit the null check on
*/
function checkIfPropertiesNull(obj: {[key: string]: unknown}, except: string[] = []): boolean {
const keys = Object.keys(obj).filter(key => !except.includes(key));
for(const key of keys){
if(obj[key] !== null){
return false;
}
}
return true;
}
console.log(checkIfPropertiesNull({ id: 1, name: 'mike', city: null }, ['id', 'name'])); // true, because id and name are not checked
console.log(checkIfPropertiesNull({ id: 1, name: 'mike', city: 'Munich' }, ['id', 'name'])); // false, city isn't null
Playground
for (let prop in Modal) {
if (Modal.prop != time && Modal.prop != id && Modal[prop] != null) {
return false
}
}
return true
Return true if all properties except ID and Time are null.
Well, you have two options in this case:
Put your loop in a function to separate this logic to your model:
function checkProperties(obj) {
for (var key in obj) {
if (obj[key] !== null && obj[key] != "")
return false;
}
return true;
}
var obj = {
x: null,
y: "",
z: 1
}
checkProperties(obj) //returns false
Use Object.values and every to check your properties as an array.
let report = {
property1: null,
property2: null,
}
let result = !Object.values(report).every(o => o === null);
console.log(result);
I am attempting to sort an array of objects by a name property that exists on each object. When using the sort() method with the code below I am getting the following error:
ERROR ReferenceError: b is not defined
Here is my code:
myArray.sort( (a, b) => {
return (typeof a.name: string === 'string') - (typeof b.name === 'string')|| a.name - b.name || a.name.localeCompare(b.name)};
Here is what is odd though...
When I run:
myArray.sort( (a, b) => {
console.log(a.name);
console.log(b.name);
It logs the names perfectly fine. What am I missing??
Just to be a thorough little bit of context:
I am using this method after doing an HTTP call from an angular service.ts file and this array is being passed to my component and subscribed to. And I am using Angular, so this would be Typescript compiling to JavaScript. I also have another myArray.forEach() method just below my sort() method and that is working.
Is this what you want?
var a = [
{ name: "John" },
{ name: "Jack" },
{ name: "Bob" }
];
a.sort(function (a, b) {
if (a.name > b.name) return 1;
if (a.name < b.name) return -1;
return 0;
});
console.log(a);
You could use a comparison which works independently of the type of string or number, by moving numerical values to top.
var array = [{ name: 20 }, { name: 21 }, { name: 2 }, { name: 11 }, { name: 1 }, { name: 'John' }, { name: 'Jane' }, { name: 'Frank' }, { name: 'Mary' },]
array.sort((a, b) => (typeof a.name === 'string') - (typeof b.name === 'string') || a.name > b.name || -(a.name < b.name));
console.log(array);
I have the following code based on docs
var arr = [
{ id: 15 },
{ x: [{ id: 777 }, { id: 'xx' }, { notidproperty: 987 }]},
{ id: 1111 }
];
function filterByID(obj) {
if ('id' in obj && typeof(obj.id) === 'number' && !isNaN(obj.id)) {
return true;
} else if (Object.prototype.toString.call(obj) === '[object Array]') { //obj type is Object, not Array
obj.filter(filterByID);
} else {
return false;
}
}
var arrByID = arr.filter(filterByID);
console.log('expected length = 3, actual length = ' + arrByID.length);
console.log(arrByID);
How can I filter 'arr' array ? Is there any other techniques to get proper result?
EDIT: the expected result is filtered array of objects that have id property with numeric value
So expected Id values are 15, 777, 1111
I suggest to use Array#reduce() instead of Array#filter(), because you need a flat array for the result, to count.
I use isFinite as check for the id.
function rr(r, a) {
Object.keys(a).forEach(function (k) {
if (Array.isArray(a[k])) {
r = r.concat(a[k].reduce(rr, []));
} else {
isFinite(a.id) && r.push(a);
}
});
return r;
}
var arr = [{ id: 15 }, { x: [{ id: 777 }, { id: 'xx' }, { notidproperty: 987 }] }, { id: 1111 }],
result = arr.reduce(rr, []);
document.write('<pre>' + JSON.stringify(result, 0, 4) + '</pre>');
You have to modify the following branch:
if (Object.prototype.toString.call(obj) === '[object Array]') { //obj type is Object, not Array
obj.filter(filterByID);
}
This only filters but returns no result. And your object with x property of Array type will not be processed here. So:
if(obj.x && Array.isArray(obj.x)){
return obj.x.filter(filterByID).length>0
}
Or:
if(obj.x && Array.isArray(obj.x)){
obj.x = obj.x.filter(filterByID)
return true
}
(Depending on what you want to do)
Almost there. In the else if you don't return anything. Instead of
obj.filter(filterByID);
try return (obj.filter(filterByID).length > 0);
because if the result of the recursive filterByID is an array with length more than 0, you want to add that to the outer result.