Skip to content

Commit

Permalink
Refactor _.min and _.max (fix jashkenas#2688)
Browse files Browse the repository at this point in the history
  • Loading branch information
jgonggrijp committed Feb 4, 2021
1 parent fca3114 commit 8743fc0
Show file tree
Hide file tree
Showing 11 changed files with 216 additions and 142 deletions.
43 changes: 43 additions & 0 deletions modules/_extremum.js
@@ -0,0 +1,43 @@
import identity from './identity.js';
import cb from './_cb.js';
import createReduce from './_createReduce.js';

// The general algorithm behind `_.min` and `_.max`. `compare` should return
// `true` if its first argument is more extreme than (i.e., should be preferred
// over) its second argument, `false` otherwise. `iteratee` and `context`, like
// in other collection functions, let you map the actual values in `collection`
// to the values to `compare`. `decide` is an optional customization point
// which is only present for historical reasons; please don't use it, as it will
// likely be removed in the future.
export default function extremum(collection, compare, iteratee, context, decide) {
decide || (decide = identity);
// Detect use of a partially-applied `extremum` as an iteratee.
if (typeof iteratee == 'number' && typeof collection[0] != 'object') {
iteratee = null;
}
iteratee = cb(iteratee, context);
// `extremum` is essentially a combined map+reduce with **two** accumulators:
// an unmapped and a mapped version, corresponding to the same element. Our
// `reduce` implementation only passes one accumulator on each iteration,
// `iterResult` (the mapped version) so we close over the second accumulator,
// `result` (the unmapped version). We define a custom `reduce` so that we can
// map the first element to the initial accumulator and also set `result`.
var result;
var reduce = createReduce(1, function(value, key) {
result = value;
return iteratee(value, key, collection);
});
var iterResult = reduce(collection, function(iterResult, value, key) {
var iterValue = iteratee(value, key, collection);
if (compare(iterValue, iterResult)) {
result = value;
return iterValue;
}
return iterResult;
});
// `extremum` normally returns an unmapped element from `collection`. However,
// `_.min` and `_.max` forcibly return a number even if there is no element
// that maps to a numeric value. Passing both accumulators through `decide`
// before returning enables this behavior.
return decide(result, iterResult);
}
19 changes: 19 additions & 0 deletions modules/_forceNumericMinMax.js
@@ -0,0 +1,19 @@
import isNaN from './isNaN.js';

// Internal comparison adapter for `_.min` and `_.max`.
// Only considers numeric values as possible extremes.
export function compareNumeric(compare) {
return function(left, right) {
if (right == null || +right !== +right) return true;
return left != null && compare(+left, +right);
}
}

// Internal `extremum` return value adapter for `_.min` and `_.max`.
// Ensures that a number is returned even if no element of the
// collection maps to a numeric value.
export function decideNumeric(fallback) {
return function(result, iterResult) {
return isNaN(+iterResult) ? fallback : result;
}
}
4 changes: 4 additions & 0 deletions modules/_greater.js
@@ -0,0 +1,4 @@
// A version of the `>` operator that can be passed around as a function.
export default function greater(left, right) {
return left > right;
}
1 change: 1 addition & 0 deletions modules/_less.js
@@ -1,3 +1,4 @@
// A version of the `<` operator that can be passed around as a function.
export default function less(left, right) {
return left < right;
}
1 change: 1 addition & 0 deletions modules/_lessEqual.js
@@ -1,3 +1,4 @@
// A version of the `<=` operator that can be passed around as a function.
export default function lessEqual(left, right) {
return left <= right;
}
34 changes: 7 additions & 27 deletions modules/max.js
@@ -1,29 +1,9 @@
import isArrayLike from './_isArrayLike.js';
import values from './values.js';
import cb from './_cb.js';
import each from './each.js';
import partial from './partial.js';
import _ from './underscore.js';
import extremum from './_extremum.js';
import { compareNumeric, decideNumeric } from './_forceNumericMinMax.js';
import greater from './_greater.js';

// Return the maximum element (or element-based computation).
export default function max(obj, iteratee, context) {
var result = -Infinity, lastComputed = -Infinity,
value, computed;
if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) {
obj = isArrayLike(obj) ? obj : values(obj);
for (var i = 0, length = obj.length; i < length; i++) {
value = obj[i];
if (value != null && value > result) {
result = value;
}
}
} else {
iteratee = cb(iteratee, context);
each(obj, function(v, index, list) {
computed = iteratee(v, index, list);
if (computed > lastComputed || computed === -Infinity && result === -Infinity) {
result = v;
lastComputed = computed;
}
});
}
return result;
}
// Forces a numeric result.
export default partial(extremum, _, compareNumeric(greater), _, _, decideNumeric(-Infinity));
34 changes: 7 additions & 27 deletions modules/min.js
@@ -1,29 +1,9 @@
import isArrayLike from './_isArrayLike.js';
import values from './values.js';
import cb from './_cb.js';
import each from './each.js';
import partial from './partial.js';
import _ from './underscore.js';
import extremum from './_extremum.js';
import { compareNumeric, decideNumeric } from './_forceNumericMinMax.js';
import less from './_less.js';

// Return the minimum element (or element-based computation).
export default function min(obj, iteratee, context) {
var result = Infinity, lastComputed = Infinity,
value, computed;
if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) {
obj = isArrayLike(obj) ? obj : values(obj);
for (var i = 0, length = obj.length; i < length; i++) {
value = obj[i];
if (value != null && value < result) {
result = value;
}
}
} else {
iteratee = cb(iteratee, context);
each(obj, function(v, index, list) {
computed = iteratee(v, index, list);
if (computed < lastComputed || computed === Infinity && result === Infinity) {
result = v;
lastComputed = computed;
}
});
}
return result;
}
// Forces a numeric result.
export default partial(extremum, _, compareNumeric(less), _, _, decideNumeric(Infinity));
109 changes: 66 additions & 43 deletions underscore-esm.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion underscore-esm.js.map

Large diffs are not rendered by default.

0 comments on commit 8743fc0

Please sign in to comment.