There are quite a few question related to the topic, but I couldn't find the right solution for my case.
var arr = [a, b, null, d, null]
and am sorting this Array with below logic
return function(a,b){
if(a === null){
return 1;
}
else if(b === null){
return -1;
}
else if(a === b){
return 0;
}
else if(ascending) {
return a < b ? -1 : 1;
}
else if(!ascending) {
return a < b ? 1 : -1;
}
};
I get the following outputs for
Ascending : [a, b, d, null,null]
Descending : [d, b, a, null,null]
Expected : [null, null,d, b, a]
What am I doing wrong?
function getSort (ascending) {
// if ascending, `null` will be pushed towards the end of the array by returning 1
var nullPosition = ascending ? 1 : -1
return function (a, b) {
// if a is null, push it towards whichever end null elements should end up
if (a == null) return nullPosition
// Note: at this point, a is non-null (previous if statement handled that case).
//
// If b is null, it must therefore be placed closer to whichever end the null
// elements should end up on. If ascending, null elements are pulled towards
// the right end of the array. If descending, null elements are pulled towards
// the left.
//
// Therefore, we return -nullPosition. If ascending, this is -1, meaning a comes
// before b; if descending, this is 1, meaning a comes after b. This is
// clearly the correct behavior, since ascending will push b, which is null,
// towards the end of the array (with -1) and descending will push b towards
// the beginning of the array.
if (b == null) return -nullPosition
// OTHERWISE, both elements are non-null, so sort normally.
// if a < b AND
// if ascending, a comes first, so return -1 == -nullPosition
// if descending, a comes after, so return -nullPosition == -(-1) == 1
if (a < b) return -nullPosition
// return the opposite of the previous condition
if (a > b) return nullPosition
// return 0 if both elements are equal
return 0
}
}
function write (arr) { arr.forEach(function (d) { document.write(d + "<br>")})}
var toSort = ['a', 'b', null, 'd', null]
var sortA = getSort(true)
var sortD = getSort(false)
document.write("<br>ASCENDING<br>")
write(toSort.sort(sortA))
document.write("<br>DESCENDING<br>")
write(toSort.sort(sortD))
You could use a two pass approach by checking null values first and then order by string.
To change the sort order, you could swap the parameters or use a negated result of the one of the function.
var data = ['a', 'b', null, 'd', null];
// ascending
data.sort(function (a, b) {
return (a === null) - (b === null) || ('' + a).localeCompare(b);
});
console.log(data);
// descending
data.sort(function (a, b) {
return (b === null) - (a === null) || ('' + b).localeCompare(a);
});
console.log(data);
.as-console-wrapper { max-height: 100% !important; top: 0; }
What about the below solution?
var arr = [null, 'e', 'a', 'b', null, 'd', null];
function sortBy(arr, ascending) {
return arr.sort((a, b) => {
if(!a) return ascending ? 1 : -1;
if(!b) return ascending ? -1 : 1;
if (ascending) return a > b ? 1 : -1;
return a > b ? -1 : 1;
})
}
const ascendingArr = sortBy(arr, true);
console.log(ascendingArr);
const decendingArr = sortBy(arr, false);
console.log(decendingArr);
Related
I am writing a function that returns the longest string in the given array. If the array is empty, it should return an empty string (""). If the array contains no strings; it should return an empty string.
function longestWord(arr) {
var filtered = arr.filter(function(el) { return typeof el == 'number' });
if (filtered.length > 0) {
return Math.min.apply(Math, filtered);
} else {
return 0;
}
}
var output = longestWord([3, 'word', 5, 'up', 3, 1]);
console.log(output); // --> must be 'word'
Right now my codes doesnt pull the word instead it pulls out the number. Any idea what am I missing?
Let's walk through your code.
The first line of your longestWord function:
var filtered = arr.filter(function(el) { return typeof el == 'number' });
will filter the input array based on typeof el === 'number', which will return an array containing only the elements of the input array which are type of === number.
Since the goal is to find the longest word, this should probably be changed to:
var filtered = arr.filter(function(el) { return typeof el === 'string' });
which will return an array of the strings in the input array.
Next, there's a check to see if the filtered array is empty. If the array is empty, you return 0. Your instructions say that if the array is empty, or if the array contains no strings, it should return an empty string. So we should change this to:
return "";
If the array is not empty, or contains strings, Math.min.apply(Math, filtered) is returned. This statement would return the minimum value of an array, so probably not what you want. After all, the goal is to return the longest string.
To do this we can use a variety of methods, here's one:
filtered.reduce(function(a, b) { return a.length > b.length ? a : b })
This statement uses the reduce() method to step through the array and return the longest item.
Putting it all together we get:
function longestWord(arr) {
var filtered = arr.filter(function(el) { return typeof el === 'string' });
if (filtered.length > 0) {
return filtered.reduce(function(a, b) { return a.length >= b.length ? a : b });
} else {
return "";
}
}
console.log(longestWord([3, 'word', 5, 'up', 3, 'testing', 1]));
console.log(longestWord([]));
console.log(longestWord([1, 2, 3, 4, 5]))
console.log(longestWord(['some', 'long', 'four', 'char', 'strs']))
This isn't the most efficient code. For that, seek other answers, especially those that use reduce, however I do find it more readable and thus easier to maintain:
function longestWord(arr) {
var filtered = arr.filter(el => typeof el === 'string')
.sort((a,b) => a.length > b.length ? -1 : 1 );
if (filtered.length)
return filtered[0];
return null;
}
var output = longestWord([3, 'word', 5, 'up', 3, 1]);
console.log(output); // --> must be 'word'
But what to do when two strings are the same length?
I assume the filter meant to be for "string"
Once you have the filtered array, you can simply use reduce to get the longest word
function longestWord(arr) {
return Array.isArray(arr) ?
arr
.filter(el => typeof el == 'string' )
.reduce((a,b) => b.length > a.length ? b : a) : '';
}
console.log(longestWord([3, 'word', 5, 'up', 3, 1, 'blah']));
Even shorter code
var longestWord = (arr) => Array.isArray(arr) ? arr.reduce((a,b) => (typeof b == 'string' && b.length > a.length) ? b : a, '') : '';
const longestWord = arr =>
arr.reduce((result, str) =>
typeof str == 'string' && result.length < str.length
? str
: result, '')
As all other answers are using filter(), reduce() and all that new and fancy methods, I'm gonna answer with the do-it-yourself method (i.e. the old fashion way).
function longestWord(arr) {
if ( ! ( typeof arr === 'array' ) ) {
console.log("That's not an array!");
return '';
}
var longest = ''; // By default, the longest word is the empty string
// Linear search
for ( var i = 0, length = arr.length; i < length; i++ ) {
var el = arr[i];
if ( typeof el === 'string' && el.length > longest.length ) {
// Words STRICTLY GREATER than the last found word.
// This way, only the first word (if two lengths match) will be considered.
longest = el;
}
}
return longest;
}
I know, this is probably NOT what you want since you're already using filtered(), but it works.
I have an observable array of objects that are used to populate a table with sortable columns.
My sorting function works perfectly and is based on the following simplification:
self.sortTheItems = function () {
self.items.sort(function (l, r) {
var rslt = l === r ? 0 : l < r ? -1 : 1;
return self.sortAscending() ? rslt : -rslt;
});
}
How could this be changed to always place values of 0 last both for ascending and descending sorting?
e.g. Unsorted values: 3,1,2,2,0,1,3,0
Descending: 3,3,2,2,1,1,0,0
Ascending: 1,1,2,2,3,3,0,0
I think you just need to make it return 1 if l is zero - see updated script below
self.items.sort(function(l, r) {
var rslt,
isAscending = self.sortAscending();
if (l === r) {
rslt = 0; // return 0 if they are equal
} else if (l === 0) {
if (isAscending) {
rslt = -1; // return -1 as this is minused below to make positive 1 (moving things to the back)
} else {
rslt = 1; // return 1 to force to end;
}
} else if (l > r) {
rslt = 1; // return 1 if l is greater than r
} else {
rslt = -1; // return -1 when l is less than r
}
return isAscending ? rslt : -rslt;
});
I believe the main issue, sorting one value always last, has been answered many times before on stack overflow. (example)
To make things more interesting, you might want to explore what knockoutjs can bring to the table.
It might be nice to include a computed sort method and a computed array of sorted items so one checkbox swaps between two methods:
// Wraps a sort method in a pre-check
const sortZeroesLast = sorter => (a, b) => {
// Check both for `0`
if (a === 0) return 1;
if (b === 0) return -1;
// If none is `0`, we can use our regular sorter
return sorter(a, b);
};
// Regular sort methods
const sortAscending = (a, b) => (a > b ? 1 : a < b ? -1 : 0);
const sortDescending = (a, b) => (a < b ? 1 : a > b ? -1 : 0);
const VM = function() {
this.items = ko.observableArray([3, 1, 1, 0, 2, 1, 5]);
// Determine which sort function to use based on `ascending` setting
this.ascending = ko.observable(false);
const sorter = ko.pureComputed(() =>
sortZeroesLast(this.ascending() ? sortAscending : sortDescending)
);
// Create a computed that updates when the items
// change, or the ascending direction
this.sortedItems = ko.pureComputed(() => this.items().sort(sorter()));
this.input = ko.observable(0);
this.addInput = () => {
this.items.push(parseFloat(this.input() || 0));
};
};
ko.applyBindings(new VM());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<label>
<input type="checkbox" data-bind="checked: ascending">
Ascending
</label>
<ul data-bind="foreach: sortedItems">
<li data-bind="text: $data"></li>
</ul>
<input type="number" data-bind="value: input"><button data-bind="click: addInput">add</button>
I need to sort an array of strings, but I need it so that null is always last. For example, the array:
var arr = [a, b, null, d, null]
When sorted ascending I need it to be sorted like [a, b, d, null, null] and when sorted descending I need it to be sorted like [d, b, a, null, null].
Is this possible? I tried the solution found below but it's not quite what I need.
How can one compare string and numeric values (respecting negative values, with null always last)?
Check out .sort() and do it with custom sorting.
Example
function alphabetically(ascending) {
return function (a, b) {
// equal items sort equally
if (a === b) {
return 0;
}
// nulls sort after anything else
if (a === null) {
return 1;
}
if (b === null) {
return -1;
}
// otherwise, if we're ascending, lowest sorts first
if (ascending) {
return a < b ? -1 : 1;
}
// if descending, highest sorts first
return a < b ? 1 : -1;
};
}
var arr = [null, "a", "z", null, "b"];
console.log(arr.sort(alphabetically(true)));
console.log(arr.sort(alphabetically(false)));
Use a custom compare function that discriminates against null values:
arr.sort(function(a, b) {
return (a===null)-(b===null) || +(a>b)||-(a<b);
});
For descending order of the non-null values, just swap a and b in the direct comparison:
arr.sort(function(a, b) {
return (a===null)-(b===null) || -(a>b)||+(a<b);
});
Ascending
arr.sort((a, b) => (a != null ? a : Infinity) - (b != null ? b : Infinity))
Descending
arr.sort((a, b) => (b != null ? b : -Infinity) - (a != null ? a : -Infinity))
(For descending order if you don't have negative values in the array, I recommend to use 0 instead of -Infinity)
The simplest approach is to handle null first, then deal with non-null cases based on the desired order:
function sortnull(arr, ascending) {
// default to ascending
if (typeof(ascending) === "undefined")
ascending = true;
const multiplier = ascending ? 1 : -1;
const sorter = function(a, b) {
if (a === b) // identical? return 0
return 0;
else if (a === null) // a is null? last
return 1;
else if (b === null) // b is null? last
return -1;
else // compare, negate if descending
return a.localeCompare(b) * multiplier;
}
return arr.sort(sorter);
}
const arr = ["a", "b", null, "d", null];
console.log(sortnull(arr)); // ascending ["a", "b", "d", null, null]
console.log(sortnull(arr, true)); // ascending ["a", "b", "d", null, null]
console.log(sortnull(arr, false)); // descending ["d", "b", "a", null, null]
If you need natural sorting for numbers, or any of the options provided by Collator (including speed enhancements and respecting locale), try this approach, based off of Paul Roub's solution, cleaned up a bit. We almost always use numeric sorting, hence the defaults...
If you are not a Typescript fan, just strip off the :type specs or copy from the snippet.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Collator
const naturalCollator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
const alphabeticCollator = new Intl.Collator(undefined, {});
function nullSort(descending: boolean = false, alphabetic: boolean = false) {
return function (a: any, b: any): number {
if (a === b) {
return 0;
}
if (a === null) {
return 1;
}
if (b === null) {
return -1;
}
let ret
if (alphabetic) {
ret = alphabeticCollator.compare(a, b)
} else {
ret = naturalCollator.compare(a, b)
}
if (descending) {
ret = -ret
}
return ret
};
}
Use it like this.
// numeric, ascending (default)
myList.sort(nullSort());
// alphabetic, descending
myList.sort(nullSort(true, true));
You can modify the factory method to take a collator instead, for greater flexibility.
function nullSort(descending: boolean = false, collator: Collator = naturalCollator)
Working Snippet
const naturalCollator = new Intl.Collator(undefined, {
numeric: true,
sensitivity: 'base'
});
const alphabeticCollator = new Intl.Collator(undefined, {});
function nullSort(descending = false, alphabetic = false) {
return function(a, b) {
if (a === b) {
return 0;
}
if (a === null) {
return 1;
}
if (b === null) {
return -1;
}
let ret
if (alphabetic) {
ret = alphabeticCollator.compare(a, b)
} else {
ret = naturalCollator.compare(a, b)
}
if (descending) {
ret = -ret
}
return ret
};
}
const items = [null, 10, 1, 100, null, 'hello', .1, null]
console.log(items.sort(nullSort()));
like this, note: this will only push the null's to the back
var arr = ["a", null, "b"];
var arrSor = [];
arr.forEach(function (el) {
if (el === null) {
arrSor.push(el);
} else {
arrSor.unshift(el);
}
});
Do it like:
var arr = [a, b, null, d, null]
foreach ($arr as $key => $value) {
if($value == null)
unset($arr[$key]);
$arr[] = $value;
}
// rebuild array index
$arr = array_values($arr);
echo '<pre>';print_r($arr);die;
I am sorting objects with a custom index and this works for me. I am not wanting to change the original array and it is important to keep the null indexes where they are.
let sorted = [...array].sort((a, b) => {
if (!a || !b) return 0;
else return a.CustomIndex - b.CustomIndex;
});
function sortNumsAsc(arr) {
if(arr === null || arr === []) {
return [];
}
else {
return arr.sort(function(a,b){return a-b});
//return newarr;
}
}
console.log(sortNumsAs([801, 19, 4, 5, -4, 85]))
I'm quite new with JavaScript but familiar with python. In Python I get this output:
In [1]: [1,9,[5,4,2]] > [1,9,[14,5,4]]
Out[1]: False
In JavaScript:
> [1,9,[5,4,2]] > [1,9,[14,5,4]]
true
It seems that the arrays are converted into a string before the comparison.
Now i wanted to write a function by myself and walk through the array an compare each element.
I came up with this coffeescript code:
compare_list = (a, b)->
if typeof a == "object" and typeof b != "object"
return 1
else if typeof a != "object" and typeof b == "object"
return -1
else if typeof a != "object" and typeof b != "object"
if a > b
return 1
else if a < b
return -1
else
return 0
else if typeof a == "object" and typeof b == "object"
for i in [0...a.length]
if i > (b.length-1)
return 1
tmp = compare_list a[i], b[i]
if tmp != 0
return tmp
if b.length > a.length
return -1
return 0
It works this way but the typeof a == "object" part looks not correct to me. Is there a simpler/better/more robust solution?
Thanks for your help.
This is basically the same algorithm avoiding the typeof operator and doing a little trick in the for loop to not check for the lengths of the arrays every time:
cmp = (a, b) -> (a > b) - (a < b)
cmpArray = (a, b)->
aIsArray = Array.isArray a
bIsArray = Array.isArray b
return cmp a, b if not aIsArray and not bIsArray
return -1 if not aIsArray and bIsArray
return 1 if aIsArray and not bIsArray
# Both are arrays.
len = Math.min a.length, b.length
for i in [0...len] by 1
if tmp = cmpArray a[i], b[i]
return tmp
a.length - b.length
Unfortunately, CoffeeScript does not provide any sort of pattern matching. That would make this code much more DRY-er. You can fake a poor man's pattern matching using a switch statement if you want:
cmpArray = (a, b)->
switch "#{Array.isArray a},#{Array.isArray b}"
when 'false,false' then (a > b) - (a < b) # Compare primitives.
when 'false,true' then -1
when 'true,false' then 1
else
len = Math.min a.length, b.length
for i in [0...len] by 1
if tmp = cmpArray a[i], b[i]
return tmp
a.length - b.length
But this is definitely not very idiomatic CoffeeScript. If CoffeeScript supported some kind of pattern matching, i'd definitely go for this kind of slution, as i think it reads very nicely being just a single expression and not relying (too much) on early returns.
Trying to solve the same issue, I too only came up with a custom solution. https://gist.github.com/ruxkor/2772234
As Javascript is using string coercion when comparing object, I think it is necessary to use a custom comparison function to simulate Python behavior.
I had a go at implementing a JavaScript function compareArrays that behaves like array comparison in Python:
function compareArrays(a, b) {
var aIsArray = Array.isArray(a),
bIsArray = Array.isArray(b),
cmp = 0;
if (!aIsArray || !bIsArray) {
throw new Error('Can\'t compare array to non-array: ' + a + ', ' + b);
}
_.find(a, function (aElem, index) {
var bElem = b[index];
if (Array.isArray(aElem) || Array.isArray(bElem)) {
cmp = compareArrays(aElem, bElem);
} else {
cmp = (aElem > bElem) - (aElem < bElem);
}
if (cmp !== 0) {
return true;
}
});
return cmp;
}
It uses Underscore to iterate over arrays, and recurses to handle nested arrays.
See my fiddle which includes a primitive test suite.
Test Results
[1,9,[5,4,2]] < [1,9,[14,5,4]]
[1,[1]] can't be compared to [1,1]
[1,[2]] > [1,[1]]
[2] > [1]
[1] == [1]
[] == []
Let's make it minimalistic:
function compare(a, b) {
if (a instanceof Array && b instanceof Array) {
for (var r, i=0, l=Math.min(a.length, b.length); i<l; i++)
if (r = compare(a[i], b[i]))
return r;
return a.length - b.length;
} else // use native comparison algorithm, including ToPrimitive conversion
return (a > b) - (a < b);
}
(using instanceof for Array detection, see this article for discussion)
If you want to make objects always greater than primitives, you can change the last line to
return (typeof a==="object")-(typeof b==="object") || (a>b)-(a<b);
It's pretty ugly, but this seems to do the job:
var compareLists = function compare(listA, listB) {
if (Array.isArray(listA)) {
if (Array.isArray(listB)) {
if (listA.length == 0) {
if (listB.length == 0) {
return 0;
} else {
return -1;
}
} else {
if (listB.length == 0) {
return +1;
} else {
return compare(listA[0], listB[0]) ||
compare(listA.slice(1), listB.slice(1));
}
}
} else {
return -1; // arbitrary decision: arrays are smaller than scalars
}
} else {
if (Array.isArray(listB)) {
return +1; // arbitrary decision: scalars are larger than arrays
} else {
return listA < listB ? -1 : listA > listB ? + 1 : 0;
}
}
};
compareLists([1, 9, [5, 4, 2]], [1, 9, [14, 5, 4]]); // -1
compareLists([1, 9, [5, 4, 2]], [1, 9, [5, 4]]); // +1
compareLists([1, 9, [5, 4, 2]], [1, 9, [5, 4, 2]]); // 0
Depending upon your environment, you might want to shim Array.isArray:
if (!Array.isArray) {
Array.isArray = function (obj) {
return Object.prototype.toString.call(obj) === "[object Array]";
};
}
For example, Hello World! and Hi World! - the first occurrence of the difference is at the second character. What would be the JavaScript/jQuery function?
Assuming, like other answers, that matching strings return -1:
// Find common prefix of strings a and b.
var prefix = function(a,b){
return a && a[0] === b[0] ? a[0] + prefix(a.slice(1), b.slice(1)) : '';
};
// Find index of first difference.
var diff = function(a,b){
return a===b ? -1 : prefix(a,b).length;
};
var tests = [
['Hello World!', 'Hi World!'],
['aaabab', 'aaabzbzz'],
['', ''],
['abc', 'abc'],
['qrs', 'tu'],
['abc', ''],
['', 'abc']
];
console.log('diff', tests.map(test => diff(test[0], test[1])));
// Or just count up to the first difference
// Trickier nested ternary to handle the -1 however.
var diff2 = function(a,b){
return a === b ? -1 : a[0] === b[0] ? 1 + diff2(a.slice(1), b.slice(1)) : 0;
};
console.log('diff2', tests.map(test => diff2(test[0], test[1])));
Maybe something like this? It returns, in that order, the position of the first
difference if there's any, the length of the shortest string if those are different, or -1 if everything is equal.
function findDiff(a, b) {
a = a.toString();
b = b.toString();
for (var i = 0; i < Math.min(a.length, b.length); i++) {
if (a.charAt(i) !== b.charAt(i)) { return i; }
}
if (a.length !== b.length) { return Math.min(a.length, b.length); }
return -1;
}
Thanks Phil for the suggestions!
function strDiff(first, second) {
if(first==second)
return -1;
first = first.toString();
second = second.toString();
var minLen = min(first.length,second.length);
for(var i = 0; i<minLen; i++) {
if(first.charAt(i) != second.charAt(i)) {
return i;
}
}
return minLen;
}
Returns -1 if the strings do not differ, or the index (starting at 0) of the character at which they do (this is the length of the shortest string if they only differ by being different lengths, e.g. 'abcd' and 'abcdef' would return 4.
function firstDiff(a, b) {
var i = 0;
while (a.charAt(i) === b.charAt(i))
if (a.charAt(i++) === '')
return -1;
return i;
}
Returns the position where the two strings a and b first differ or -1 if they are equal.
A more efficient but less readable version:
function firstDiff(a, b) {
for (var i = 0, c; (c = a.charAt(i)) === b.charAt(i); ++i)
if (c === '')
return -1;
return i;
}
If you feel that you should first stringify the arguments, then do it in the invocation:
firstDiff(toString(a), toString(b))
Most often that will be a waste of time. Know your data!