Repository: plaid/sanctuary
Pull request #146 (2016-02-04 06:04:47)

number, string: add S.min and S.max
```haskell
min :: Ord a => a -> a -> a
max :: Ord a => a -> a -> a
```

[`R.min`][1] and [`R.max`][2], but with type checking. I want to tick some boxes in #145. :)


[1]: http://ramdajs.com/docs/#min
[2]: http://ramdajs.com/docs/#max



Patch:
diff --git a/index.js b/index.js
index 8f3c0a8..fd44600 100644
--- a/index.js
+++ b/index.js
@@ -169,6 +169,16 @@
     }
   );
 
+  //  Ord :: TypeClass
+  var Ord = $.TypeClass(
+    'sanctuary/Ord',
+    function(x) {
+      var type = _type(x);
+      return type === 'Number' && $.ValidNumber.test(x) ||
+             type === 'String';
+    }
+  );
+
   //  Semigroup :: TypeClass
   var Semigroup = $.TypeClass(
     'sanctuary/Semigroup',
@@ -2347,6 +2357,52 @@
       [$.FiniteNumber, $.NonZeroFiniteNumber, $.FiniteNumber],
       function(a, b) { return a / b; });
 
+  //# min :: Ord a => a -> a -> a
+  //.
+  //. Returns the smaller of its two arguments.
+  //.
+  //. Strings are compared lexicographically. Specifically, the Unicode
+  //. code point value of each character in the first string is compared
+  //. to the value of the corresponding character in the second string.
+  //.
+  //. See also [`max`](#max).
+  //.
+  //. ```javascript
+  //. > S.min(10, 2)
+  //. 2
+  //.
+  //. > S.min('10', '2')
+  //. '10'
+  //. ```
+  S.min =
+  def('min',
+      {a: [Ord]},
+      [a, a, a],
+      function(x, y) { return x < y ? x : y; });
+
+  //# max :: Ord a => a -> a -> a
+  //.
+  //. Returns the larger of its two arguments.
+  //.
+  //. Strings are compared lexicographically. Specifically, the Unicode
+  //. code point value of each character in the first string is compared
+  //. to the value of the corresponding character in the second string.
+  //.
+  //. See also [`min`](#min).
+  //.
+  //. ```javascript
+  //. > S.max(10, 2)
+  //. 10
+  //.
+  //. > S.max('10', '2')
+  //. '2'
+  //. ```
+  S.max =
+  def('max',
+      {a: [Ord]},
+      [a, a, a],
+      function(x, y) { return x > y ? x : y; });
+
   //. ### Integer
 
   //# even :: Integer -> Boolean


Test patch:
diff --git a/test/index.js b/test/index.js
index 53c510a..de08d41 100644
--- a/test/index.js
+++ b/test/index.js
@@ -3575,6 +3575,94 @@ describe('number', function() {
 
   });
 
+  describe('min', function() {
+
+    it('is a binary function', function() {
+      eq(typeof S.min, 'function');
+      eq(S.min.length, 2);
+    });
+
+    it('type checks its arguments', function() {
+      assert.throws(function() { S.min(/x/); },
+                    errorEq(TypeError,
+                            '‘min’ requires ‘a’ to implement Ord; ' +
+                            'RegExp does not'));
+
+      assert.throws(function() { S.min(NaN); },
+                    errorEq(TypeError,
+                            '‘min’ requires ‘a’ to implement Ord; ' +
+                            'Number does not'));
+    });
+
+    it('may be applied to (valid) numbers', function() {
+      eq(S.min(10, 2), 2);
+      eq(S.min(2, 10), 2);
+      eq(S.min(0.1, 0.01), 0.01);
+      eq(S.min(0.01, 0.1), 0.01);
+      eq(S.min(Infinity, -Infinity), -Infinity);
+      eq(S.min(-Infinity, Infinity), -Infinity);
+    });
+
+    it('may be applied to strings', function() {
+      eq(S.min('abc', 'xyz'), 'abc');
+      eq(S.min('xyz', 'abc'), 'abc');
+      eq(S.min('10', '2'), '10');
+      eq(S.min('2', '10'), '10');
+      eq(S.min('A', 'a'), 'A');
+      eq(S.min('a', 'A'), 'A');
+    });
+
+    it('is curried', function() {
+      eq(S.min(10).length, 1);
+      eq(S.min(10)(2), 2);
+    });
+
+  });
+
+  describe('max', function() {
+
+    it('is a binary function', function() {
+      eq(typeof S.max, 'function');
+      eq(S.max.length, 2);
+    });
+
+    it('type checks its arguments', function() {
+      assert.throws(function() { S.max(/x/); },
+                    errorEq(TypeError,
+                            '‘max’ requires ‘a’ to implement Ord; ' +
+                            'RegExp does not'));
+
+      assert.throws(function() { S.max(NaN); },
+                    errorEq(TypeError,
+                            '‘max’ requires ‘a’ to implement Ord; ' +
+                            'Number does not'));
+    });
+
+    it('may be applied to (valid) numbers', function() {
+      eq(S.max(10, 2), 10);
+      eq(S.max(2, 10), 10);
+      eq(S.max(0.1, 0.01), 0.1);
+      eq(S.max(0.01, 0.1), 0.1);
+      eq(S.max(Infinity, -Infinity), Infinity);
+      eq(S.max(-Infinity, Infinity), Infinity);
+    });
+
+    it('may be applied to strings', function() {
+      eq(S.max('abc', 'xyz'), 'xyz');
+      eq(S.max('xyz', 'abc'), 'xyz');
+      eq(S.max('10', '2'), '2');
+      eq(S.max('2', '10'), '2');
+      eq(S.max('A', 'a'), 'a');
+      eq(S.max('a', 'A'), 'a');
+    });
+
+    it('is curried', function() {
+      eq(S.max(10).length, 1);
+      eq(S.max(10)(2), 10);
+    });
+
+  });
+
 });
 
 describe('integer', function() {