diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30efe19 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/bower_components/ +/node_modules/ +/.pulp-cache/ +/output/ +/generated-docs/ +/.psc-package/ +/.psc* +/.purs* +/.psa* +/.spago diff --git a/README.md b/README.md new file mode 100644 index 0000000..4fb96e5 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# purescript-math-es6 + +Currently [purescript-math](https://github.com/purescript/purescript-math) has +limited support for the functions added to the Javascript `Math` object in ES6. +This library implements the missing functions and also provides polyfills for +them. diff --git a/packages.dhall b/packages.dhall new file mode 100644 index 0000000..2e7ce02 --- /dev/null +++ b/packages.dhall @@ -0,0 +1,104 @@ +{- +Welcome to your new Dhall package-set! + +Below are instructions for how to edit this file for most use +cases, so that you don't need to know Dhall to use it. + +## Use Cases + +Most will want to do one or both of these options: +1. Override/Patch a package's dependency +2. Add a package not already in the default package set + +This file will continue to work whether you use one or both options. +Instructions for each option are explained below. + +### Overriding/Patching a package + +Purpose: +- Change a package's dependency to a newer/older release than the + default package set's release +- Use your own modified version of some dependency that may + include new API, changed API, removed API by + using your custom git repo of the library rather than + the package set's repo + +Syntax: +where `entityName` is one of the following: +- dependencies +- repo +- version +------------------------------- +let upstream = -- +in upstream + with packageName.entityName = "new value" +------------------------------- + +Example: +------------------------------- +let upstream = -- +in upstream + with halogen.version = "master" + with halogen.repo = "https://example.com/path/to/git/repo.git" + + with halogen-vdom.version = "v4.0.0" + with halogen-vdom.dependencies = [ "extra-dependency" ] # halogen-vdom.dependencies +------------------------------- + +### Additions + +Purpose: +- Add packages that aren't already included in the default package set + +Syntax: +where `` is: +- a tag (i.e. "v4.0.0") +- a branch (i.e. "master") +- commit hash (i.e. "701f3e44aafb1a6459281714858fadf2c4c2a977") +------------------------------- +let upstream = -- +in upstream + with new-package-name = + { dependencies = + [ "dependency1" + , "dependency2" + ] + , repo = + "https://example.com/path/to/git/repo.git" + , version = + "" + } +------------------------------- + +Example: +------------------------------- +let upstream = -- +in upstream + with benchotron = + { dependencies = + [ "arrays" + , "exists" + , "profunctor" + , "strings" + , "quickcheck" + , "lcg" + , "transformers" + , "foldable-traversable" + , "exceptions" + , "node-fs" + , "node-buffer" + , "node-readline" + , "datetime" + , "now" + ] + , repo = + "https://github.com/hdgarrood/purescript-benchotron.git" + , version = + "v7.0.0" + } +------------------------------- +-} +let upstream = + https://github.com/purescript/package-sets/releases/download/psc-0.14.3-20210722/packages.dhall sha256:1ceb43aa59436bf5601bac45f6f3781c4e1f0e4c2b8458105b018e5ed8c30f8c + +in upstream diff --git a/spago.dhall b/spago.dhall new file mode 100644 index 0000000..2e0e6dc --- /dev/null +++ b/spago.dhall @@ -0,0 +1,17 @@ +{- +Welcome to a Spago project! +You can edit this file as you like. + +Need help? See the following resources: +- Spago documentation: https://github.com/purescript/spago +- Dhall language tour: https://docs.dhall-lang.org/tutorials/Language-Tour.html + +When creating a new Spago project, you can use +`spago init --no-comments` or `spago init -C` +to generate this file without the comments in this block. +-} +{ name = "my-project" +, dependencies = [ "console", "effect", "prelude", "psci-support" ] +, packages = ./packages.dhall +, sources = [ "src/**/*.purs", "test/**/*.purs" ] +} diff --git a/src/Math/ES6.purs b/src/Math/ES6.purs new file mode 100644 index 0000000..58d758c --- /dev/null +++ b/src/Math/ES6.purs @@ -0,0 +1,24 @@ +module Math.ES6 + ( module Math.Internal.ES6 + ) where + +import Math.Internal.ES6 + ( acosh + , asinh + , atanh + , cbrt + , clz32 + , cosh + , expm1 + , fround + , hypot + , hypot2 + , imul + , log1p + , log2 + , log10 + , sign + , sinh + , tanh + , trunc + ) diff --git a/src/Math/Internal/ES6.js b/src/Math/Internal/ES6.js new file mode 100644 index 0000000..a27a65b --- /dev/null +++ b/src/Math/Internal/ES6.js @@ -0,0 +1,296 @@ +"use strict"; + +// acosh ----------------------------------------------------------------------- +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/acosh +exports.acoshNative = Math.acosh; + +// Polyfill from MDN +exports.acoshPolyfill = function(x) { + return Math.log(x + Math.sqrt(x * x - 1)); +}; + +exports.acosh = Math.acosh ? Math.acosh : exports.acoshPolyfill; + +// asinh ----------------------------------------------------------------------- +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/asinh +exports.asinhNative = Math.asinh; + +// Polyfill from MDN +exports.asinhPolyfill = function(x) { + var absX = Math.abs(x), w + if (absX < 3.725290298461914e-9) // |x| < 2^-28 + return x + if (absX > 268435456) // |x| > 2^28 + w = Math.log(absX) + Math.LN2 + else if (absX > 2) // 2^28 >= |x| > 2 + w = Math.log(2 * absX + 1 / (Math.sqrt(x * x + 1) + absX)) + else + var t = x * x, w = Math.log1p(absX + t / (1 + Math.sqrt(1 + t))) + + return x > 0 ? w : -w +}; + +exports.asinh = Math.asinh ? Math.asinh : exports.asinhPolyfill; + +// atanh ----------------------------------------------------------------------- +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atanh +exports.atanhNative = Math.atanh; + +// Polyfill from MDN +exports.atanhPolyfill = function(x) { + return Math.log((1+x)/(1-x)) / 2; +}; + +exports.atanh = Math.atanh ? Math.atanh : exports.atanhPolyfill; + +// cbrt ------------------------------------------------------------------------ +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cbrt +exports.cbrtNative = Math.cbrt; + +// Polyfill from MDN +exports.cbrtPolyfill = (function(pow) { + return function cbrt(x){ + // ensure negative numbers remain negative: + return x < 0 ? -pow(-x, 1/3) : pow(x, 1/3); + }; + })(Math.pow); // localize Math.pow to increase efficiency + +exports.cbrt = Math.cbrt ? Math.cbrt : exports.cbrtPolyfill; + +// clz32 ----------------------------------------------------------------------- +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32 +exports.clz32Native = Math.clz32; + +exports.clz32Polyfill = (function(log, LN2){ + return function(x) { + // Let n be ToUint32(x). + // Let p be the number of leading zero bits in + // the 32-bit binary representation of n. + // Return p. + var asUint = x >>> 0; + if (asUint === 0) { + return 32; + } + return 31 - (log(asUint) / LN2 | 0) |0; // the "| 0" acts like math.floor + }; +})(Math.log, Math.LN2); + +exports.clz32 = Math.clz32 ? Math.clz32 : exports.clz32Polyfill; + +// cosh ------------------------------------------------------------------------ +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cosh +exports.coshNative = Math.cosh; + +// Polyfill from MDN +exports.coshPolyfill = function(x) { + var y = Math.exp(x); + return (y + 1 / y) / 2; +}; + +exports.cosh = Math.cosh ? Math.cosh : exports.coshPolyfill; + +// expm1 ----------------------------------------------------------------------- +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/expm1 +exports.expm1Native = Math.expm1; + +// MDN does not provide a polyfill, so we do the obvious one based on the +// definition +exports.expm1Polyfill = function(x) { + return Math.exp(x) - 1; +}; + +exports.expm1 = Math.expm1 ? Math.expm1 : exports.expm1Polyfill; + +// fround ---------------------------------------------------------------------- +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/fround +exports.froundNative = Math.fround; + +// Polyfill from MDN +exports.froundPolyfill = (function (array) { + return function(x) { + return array[0] = x, array[0]; + }; +})(new Float32Array(1)); + +exports.fround = Math.fround ? Math.fround : exports.froundPolyfill; + +// hypot ----------------------------------------------------------------------- +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/hypot + +// hypotNative needs to take an array so we provide a wrapper that converts +// the array into an argument list +exports.hypotNative = function (array) { + return Math.hypot(...array); + // The ... operator was introduced in ES6. It should exist anytime Math.hypot + // is defined. If Math.hypot does not exist you will be calling hypotPolyfill + // anyway +}; + +// Polyfill based on MDN, with small alterations to take an array rather than +// an argument list +exports.hypotPolyfill = function (array) { // Explicit argument `array` added + var max = 0; + var s = 0; + var containsInfinity = false; + for (var i = 0; i < array.length; ++i) { // `arguments` replaced with `array` + var arg = Math.abs(array[i]); // `Number(arguments[i])` replaced with `array[i]` + if (arg === Infinity) + containsInfinity = true + if (arg > max) { + s *= (max / arg) * (max / arg); + max = arg; + } + s += arg === 0 && max === 0 ? 0 : (arg / max) * (arg / max); + } + return containsInfinity ? Infinity : (max === 1 / 0 ? 1 / 0 : max * Math.sqrt(s)); +}; + +exports.hypot = Math.hypot ? exports.hypotNative : exports.hypotPolyfill; + +// hypot2 ---------------------------------------------------------------------- +// A more efficient and convenient function for calls to hypot with only two +// elements in the array. So `hypot2(2)(3) = hypot([2, 3])` +exports.hypot2Native = function (x) { + return function (y) { + return Math.hypot(x, y); + }; +}; + +// This is the function hypotPolyfill from above with the loop unwound and +// specialized to x and y rather than arg +exports.hypot2Polyfill = function (x) { + return function (y) { + var max = 0; + var s = 0; + var containsInfinity = false; + + if (x === Infinity) + containsInfinity = true + if (x > max) { + s *= (max / x) * (max / x); + max = x; + } + s += x === 0 && max === 0 ? 0 : (x / max) * (x / max); + + if (y === Infinity) + containsInfinity = true + if (y > max) { + s *= (max / y) * (max / y); + max = y; + } + s += y === 0 && max === 0 ? 0 : (y / max) * (y / max); + + return containsInfinity ? Infinity : (max === 1 / 0 ? 1 / 0 : max * Math.sqrt(s)); + }; +}; + +exports.hypot2 = Math.hypot ? exports.hypot2Native : exports.hypot2Polyfill; + +// imul ------------------------------------------------------------------------ +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul + +// imul takes two arguments, so a curried wrapper is required +exports.imulNative = function (opA) { + return function (opB) { + return Math.imul(opA, opB); + }; +}; + +// Polyfill directly from MDN, converted to curried +exports.imulPolyfill = function(opA) { + return function (opB) { + opB |= 0; // ensure that opB is an integer. opA will automatically be coerced. + // floating points give us 53 bits of precision to work with plus 1 sign bit + // automatically handled for our convienence: + // 1. 0x003fffff /*opA & 0x000fffff*/ * 0x7fffffff /*opB*/ = 0x1fffff7fc00001 + // 0x1fffff7fc00001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/ + var result = (opA & 0x003fffff) * opB; + // 2. We can remove an integer coersion from the statement above because: + // 0x1fffff7fc00001 + 0xffc00000 = 0x1fffffff800001 + // 0x1fffffff800001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/ + if (opA & 0xffc00000 /*!== 0*/) result += (opA & 0xffc00000) * opB |0; + return result |0; + }; +}; + +exports.imul = Math.imul ? exports.imulNative : exports.imulPolyfill; + +// log1p ----------------------------------------------------------------------- +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log1p +exports.log1pNative = Math.log1p; + +// MDN does not supply a polyfill so we implement the obvious one based on the +// definition +exports.log1pPolyfill = function (x) { + return Math.log(x + 1); +} + +exports.log1p = Math.log1p ? Math.log1p : exports.log1pPolyfill; + +// log2 ------------------------------------------------------------------------ +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log2 +exports.log2Native = Math.log2; + +// Polyfill from MDN +exports.log2Polyfill = function(x) { + return Math.log(x) * Math.LOG2E; +}; + +exports.log2 = Math.log2 ? Math.log2 : exports.log2Polyfill; + +// log10 ----------------------------------------------------------------------- +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log10 +exports.log10Native = Math.log10; + +// Polyfill from MDN +exports.log10Polyfill = function(x) { + return Math.log(x) * Math.LOG10E; +}; + +exports.log10 = Math.log10 ? Math.log10 : exports.log10Polyfill; + +// sign ------------------------------------------------------------------------ +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sign +exports.signNative = Math.sign; + +// MDN does not provide a polyfill. We implement a simple one below, taking care +// to ensure the correct return values for -0, +0 and NaN. The `x !== x` test +// checks for NaN in a way that is likely to work reliabily in older browsers +exports.signPolyfill = function(x) { + return x === 0 || x !== x ? x : (x < 0 ? -1 : 1); +} + +exports.sign = Math.sign ? Math.sign : exports.signPolyfill; + +// sinh ------------------------------------------------------------------------ +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sinh +exports.sinhNative = Math.sinh; + +// Polyfill from MDN +exports.sinhPolyfill = function(x) { + var y = Math.exp(x); + return (y - 1 / y) / 2; +}; + +exports.sinh = Math.sinh ? Math.sinh : exports.sinhPolyfill; + +// tanh ------------------------------------------------------------------------ +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/tanh +exports.tanhNative = Math.tanh; + +exports.tanhPolyfill = function(x) { + var a = Math.exp(+x), b = Math.exp(-x); + return a == Infinity ? 1 : b == Infinity ? -1 : (a - b) / (a + b); +} + +exports.tanh = Math.tanh ? Math.tanh : exports.tanhPolyfill; + +// trunc ----------------------------------------------------------------------- +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc +exports.truncNative = Math.trunc; + +exports.truncPolyfill = function(x) { + return x === 0 ? x : (x < 0 ? Math.ceil(x) : Math.floor(x)); +}; + +exports.trunc = Math.trunc ? Math.trunc : exports.truncPolyfill; diff --git a/src/Math/Internal/ES6.purs b/src/Math/Internal/ES6.purs new file mode 100644 index 0000000..a423308 --- /dev/null +++ b/src/Math/Internal/ES6.purs @@ -0,0 +1,132 @@ +module Math.Internal.ES6 + ( Radians + , acosh, acoshNative, acoshPolyfill + , asinh, asinhNative, asinhPolyfill + , atanh, atanhNative, atanhPolyfill + , cbrt, cbrtNative, cbrtPolyfill + , clz32, clz32Native, clz32Polyfill + , cosh, coshNative, coshPolyfill + , expm1, expm1Native, expm1Polyfill + , fround, froundNative, froundPolyfill + , hypot, hypotNative, hypotPolyfill + , hypot2, hypot2Native, hypot2Polyfill + , imul, imulNative, imulPolyfill + , log1p, log1pNative, log1pPolyfill + , log2, log2Native, log2Polyfill + , log10, log10Native, log10Polyfill + , sinh, sinhNative, sinhPolyfill + , sign, signNative, signPolyfill + , tanh, tanhNative, tanhPolyfill + , trunc, truncNative, truncPolyfill + ) where + +-- | An alias to make types in this module more explicit. +type Radians = Number + +foreign import acoshNative :: Number -> Radians + +foreign import acoshPolyfill :: Number -> Radians + +foreign import acosh :: Number -> Radians + +foreign import asinhNative :: Number -> Radians + +foreign import asinhPolyfill :: Number -> Radians + +foreign import asinh :: Number -> Radians + +foreign import atanhNative :: Number -> Radians + +foreign import atanhPolyfill :: Number -> Radians + +foreign import atanh :: Number -> Radians + +foreign import cbrtNative :: Number -> Number + +foreign import cbrtPolyfill :: Number -> Number + +foreign import cbrt :: Number -> Number + +foreign import clz32Native :: Int -> Int + +foreign import clz32Polyfill :: Int -> Int + +foreign import clz32 :: Int -> Int + +foreign import coshNative :: Radians -> Number + +foreign import coshPolyfill :: Radians -> Number + +foreign import cosh :: Radians -> Number + +foreign import expm1Native :: Number -> Number + +foreign import expm1Polyfill :: Number -> Number + +foreign import expm1 :: Number -> Number + +foreign import froundNative :: Number -> Number + +foreign import froundPolyfill :: Number -> Number + +foreign import fround :: Number -> Number + +foreign import hypotNative :: Array Number -> Number + +foreign import hypotPolyfill :: Array Number -> Number + +foreign import hypot :: Array Number -> Number + +foreign import hypot2Native :: Number -> Number -> Number + +foreign import hypot2Polyfill :: Number -> Number -> Number + +foreign import hypot2 :: Number -> Number -> Number + +foreign import imulNative :: Int -> Int -> Int + +foreign import imulPolyfill :: Int -> Int -> Int + +foreign import imul :: Int -> Int -> Int + +foreign import log1pNative :: Number -> Number + +foreign import log1pPolyfill :: Number -> Number + +foreign import log1p :: Number -> Number + +foreign import log2Native :: Number -> Number + +foreign import log2Polyfill :: Number -> Number + +foreign import log2 :: Number -> Number + +foreign import log10Native :: Number -> Number + +foreign import log10Polyfill :: Number -> Number + +foreign import log10 :: Number -> Number + +foreign import signNative :: Number -> Number + +foreign import signPolyfill :: Number -> Number + +foreign import sign :: Number -> Number + +foreign import sinhNative :: Number -> Number + +foreign import sinhPolyfill :: Number -> Number + +foreign import sinh :: Number -> Number + +foreign import tanhNative :: Number -> Number + +foreign import tanhPolyfill :: Number -> Number + +foreign import tanh :: Number -> Number + +foreign import truncNative :: Number -> Int + +foreign import truncPolyfill :: Number -> Int + +foreign import trunc :: Number -> Int diff --git a/test/Main.purs b/test/Main.purs new file mode 100644 index 0000000..0610b8a --- /dev/null +++ b/test/Main.purs @@ -0,0 +1,51 @@ +module Test.Main where + +import Prelude + +import Effect (Effect) +import Effect.Class.Console (log) +import Math.Internal.ES6 + ( acoshNative, acoshPolyfill + , asinhNative, asinhPolyfill + , atanhNative, atanhPolyfill + , cbrtNative, cbrtPolyfill + , clz32Native, clz32Polyfill + , coshNative, coshPolyfill + , expm1Native, expm1Polyfill + , froundNative, froundPolyfill + , hypotNative, hypotPolyfill + , hypot2Native, hypot2Polyfill + , imulNative, imulPolyfill + , log1pNative, log1pPolyfill + , log2Native, log2Polyfill + , log10Native, log10Polyfill + , signNative, signPolyfill + , sinhNative, sinhPolyfill + , tanhNative, tanhPolyfill + , truncNative, truncPolyfill + ) + +main :: Effect Unit +main = do + log $ "Below are very basic tests to check that the native functions " <> + "and the polyfill functions give similar results. Numbers represent " <> + "the difference between the native and polyfill version and should " <> + "be equal to or near zero" + log $ "acosh: " <> show (acoshNative 2.0 - acoshPolyfill 2.0) + log $ "asinh: " <> show (asinhNative 2.0 - asinhPolyfill 2.0) + log $ "atanh: " <> show (atanhNative 0.5 - atanhPolyfill 0.5) + log $ "cbrt: " <> show (cbrtNative 2.0 - cbrtPolyfill 2.0) + log $ "clz32: " <> show (clz32Native 1001 - clz32Polyfill 1001) + log $ "cosh: " <> show (coshNative 2.0 - coshPolyfill 2.0) + log $ "expm1: " <> show (expm1Native 2.0 - expm1Polyfill 2.0) + log $ "fround: " <> show (froundNative 2.0 - froundPolyfill 2.0) + log $ "hypot: " <> show (hypotNative [2.0, 3.0] - hypotPolyfill [2.0, 3.0]) + log $ "hypot2: " <> show (hypot2Native 2.0 3.0 - hypot2Polyfill 2.0 3.0) + log $ "imul: " <> show (imulNative 2 (-3) - imulPolyfill 2 (-3)) + log $ "log1p: " <> show (log1pNative 2.0 - log1pPolyfill 2.0) + log $ "log2: " <> show (log2Native 2.0 - log2Polyfill 2.0) + log $ "log10: " <> show (log10Native 2.0 - log10Polyfill 2.0) + log $ "sign: " <> show (signNative (-2.0) - signPolyfill (-2.0)) + log $ "sinh: " <> show (sinhNative 2.0 - sinhPolyfill 2.0) + log $ "tanh: " <> show (tanhNative 0.5 - tanhPolyfill 0.5) + log $ "trunc: " <> show (truncNative 5.5 - truncPolyfill 5.5)