0

I'm having a hard time finding the keywords to search for this online.

I've created a class with safe math functions. Each function takes 2 arguments and after being evaluated by an assertion, it returns the result.

Example:

class SafeMath {

  static add(x: number, y: number) {
    let z: number = x + y;
    assert(z >= x, 'ds-math-add-overflow');
    return z;
  }

  static sub(x: number, y: number) {
    let z: number = x - y;
    assert(z <= x, 'ds-math-sub-underflow');
    return z;
  }

  static mul(x: number, y: number) {
    let z: number = x * y;
    assert(y == 0 || z / y == x, 'ds-math-mul-overflow');
    return z;
  }

  static div(x: number, y: number) {
    let z: number = x / y;
    assert(x > 0 || y > 0, 'ds-math-div-by-zero');
    return z;
  }

}

console.log(SafeMath.add(2,2)); // 4
console.log(SafeMath.sub(2,2)); // 0
console.log(SafeMath.mul(2,2)); // 4
console.log(SafeMath.div(2,2)); // 1

My goal was to have these functions work like this, for example:

let balance0: number = 1;
let balance1: number = 1;

let amount0In: number = 10;
let amount1In: number = 10;

let balance0Adjusted: number = balance0.mul(1000).sub(amount0In.mul(3));
let balance1Adjusted: number = balance1.mul(1000).sub(amount1In.mul(3));

...the functions would take in y and use the previous number as x.

suchislife
  • 4,251
  • 10
  • 47
  • 78
  • 1
    `1` is a number, it doesn't have a `mul` method. If you start with `let balance0: SafeNumber = new SafeNumber(1);`, you can define your own methods in a `SafeNumber` class. – Bergi Jul 31 '21 at 22:59
  • Btw, are you trying to represent unsigned integers? JS uses floating point numbers for everything, which do not overflow. And notice your `add` and `sub` method don't work with negative integers. – Bergi Jul 31 '21 at 23:00
  • Correct on Unsigned Integers. Correct on `add` and `sub` intentionally not working with negative integers. – suchislife Jul 31 '21 at 23:03
  • Ok, but `x + y` is not addition of unsigned integers. – Bergi Jul 31 '21 at 23:05

3 Answers3

2

You can make some wrapper for that:

if (!Number.prototype.mul)  // check that the mul method does not already exist 
  {
  Number.prototype.mul = function(n){ return this * n }
  }
  
if (!Number.prototype.add)
  {
  Number.prototype.add = function(n){ return this + n }
  }
  
  
let val = 5
let doubleValPlus500 = val.mul(2).add(500)

console.log( doubleValPlus500 )
Mister Jojo
  • 20,093
  • 6
  • 21
  • 40
1

You can modify Number.prototype to add functions so that you can chain these operations. Doing so using string property keys is generally considered a bad practice (see Why is extending native objects a bad practice?). You can use unique symbol property keys instead of string property keys to avoid name conflicts, etc.

Here is an example module which safely "extends" Number.prototype with a multiplication function using a unique symbol and adds to the TypeScript Number interface the new function signature:

mul.ts

const mul = Symbol("multiply");

function value(this: number, n: number) {
  return this * n;
}

declare global {
  interface Number {
    [mul]: typeof value;
  }
}

Object.defineProperty(Number.prototype, mul, { value });

export default mul;

After defining a module like the above for subtraction, addition, division, etc. you can then import the modules and use their exported unique symbols to chain operations:

import mul from "./mul.ts";
import sub from "./sub.ts";

const balance = 1;
const amountIn = 10;
const balanceAdjusted = balance[mul](1000)[sub](amountIn[mul](3));
console.log(balanceAdjusted);
970

A nicety of making these math operations chainable is that you can combine them with the optional chaining operator whenever you're dealing with nullish values which can come in handy sometimes.


The same can be done without using symbols but it isn't safe against future versions of JavaScript that might define their own Number methods for mul, etc:

mul.ts

function value(this: number, n: number) {
  return this * n;
}

declare global {
  interface Number {
    mul: typeof value;
  }
}

Object.defineProperty(Number.prototype, "mul", { value });

export {}; // you have to import or export something to make it a module
import "./mul.ts";
import "./sub.ts";

const balance = 1;
const amountIn = 10;
const balanceAdjusted = balance.mul(1000).sub(amountIn.mul(3));
console.log(balanceAdjusted);
970

Importing all of these modules individually may not be very convenient so you can also make a single module to combine all the others:

math.ts

export { default as mul } from "./mul.ts";
export { default as sub } from "./sub.ts";
/* and so forth */

Then you can import it and select the ones you want to use:

import { mul, sub } from "./math.ts";

const balance = 1;
const amountIn = 10;
const balanceAdjusted = balance[mul](1000)[sub](amountIn[mul](3));
console.log(balanceAdjusted);
mfulton26
  • 29,956
  • 6
  • 64
  • 88
  • Very handy. Very sometimes. I don't suppose it's possible to do it with dots eh? – suchislife Aug 02 '21 at 03:51
  • It is by using `mul` instead of `[mul]` but then if `Number.prototype` later defines its own `mul` method it could cause issues if it behaves differently, etc. – mfulton26 Aug 02 '21 at 09:11
  • I've updated my answer with unsafe example code added. – mfulton26 Aug 02 '21 at 09:16
  • 1
    Sweet. Lastly, I am trying to find a way to have just one import containing all functions. Not one for every single math function I create/wanna use. When I try to change the function name from `value` to `multiply` I get and error. Keep in mind I am aware of `Number.prototype.add =...` but like you safe approach. – suchislife Aug 02 '21 at 12:48
  • Personally I would define them all in separate modules/files and then have a single module/file that combines them all using multiple re-export statements like `export { default as mul } from "./mul.ts";` and do forth; that way you can import all of them from one module and use the ones you want – mfulton26 Aug 03 '21 at 23:53
0

Number.prototype based example

import { assert } from "https://deno.land/std@0.102.0/testing/asserts.ts";

declare global {

  /*
    Augument global Number.prototype with the following custom functions

    Warning - While this may look like a clean approach, it is considered 
    unsafe due to javascript possibly choosing to natively implement these
    exact function names in the near future. To avoid this, choose unique
    function names.

  */
  interface Number {
    add(n: number): number;
    sub(n: number): number;
    mul(n: number): number;
    div(n: number): number;
    pow(n: number): number;
    sqrt(): number;
    print(): number;
  }
}

Number.prototype.add = function(this:number, n:number) {

  assert((this + n) >= this, 'ds-math-add-overflow');
  return this + n;
}

Number.prototype.sub = function(this:number, n:number) {

  assert((this - n) <= this, 'ds-math-sub-underflow');
  return this - n;
}

Number.prototype.mul = function(this:number, n:number) {

  assert(n == 0 || (this * n) / n == this, 'ds-math-mul-overflow');
  return this * n;
}

Number.prototype.div = function(this:number, n:number) {

  assert(this > 0 || n > 0, 'ds-math-div-by-zero');
  return this / n;
}

Number.prototype.pow = function(this:number, n:number) {

  assert(this > 0 && n >= 2, 'ds-math-exp-to-zero');
  return this ** n;
}

// babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)
Number.prototype.sqrt = function(this:number) {

  assert(this > 0, 'ds-math-sqrt-of-zero');

  let x: number = 0;
  let y: number = this;
  let z: number = 0;

  if (y > 3) {
    z = y;
    x = y / 2 + 1;
    while (x < z) {
      z = x;
      x = (y / x + x) / 2;
    }
  } else if (y != 0) {
    z = 1;
  }
  return z;
}

Number.prototype.print = function(this:number) {

  console.log('=', this);
  return this;
}

// Here it is in action:

let balance = 0;

balance.add(10).print().sub(1).print().mul(2).print().div(3).print().pow(4).print().sqrt().print();

Output:

= 10
= 9
= 18
= 6
= 1296
= 36

Class based example

import { assert } from "https://deno.land/std@0.102.0/testing/asserts.ts";

class SafeMath {
  private n: number;

  constructor(start: number = 0) {
    this.n = start;
  }

  public add(y: number) {

    assert(this.n + y >= this.n, 'ds-math-add-overflow');

    let z: number = this.n + y;

    this.n = this.n + y;
    return this;
  }

  public sub(y: number) {

    assert(this.n - y <= this.n, 'ds-math-sub-underflow');

    let z: number = this.n - y;

    this.n = this.n - y;
    return this;
  }

  public mul(y: number) {

    assert(y == 0 || (this.n * y) / y == this.n, 'ds-math-mul-overflow');

    let z: number = this.n * y;

    this.n = this.n * y;
    return this;
  }

  public div(y: number) {

    assert(this.n > 0 || y > 0, 'ds-math-div-by-zero');

    let z: number = this.n / y;

    this.n = this.n / y;
    return this;
  }

  public pow(y: number) {

    assert(this.n > 0 && y >= 2, 'ds-math-exp-to-zero');

    let z: number = this.n ** y;
    this.n = this.n ** y;
    return this;
  }

  // babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)
  public sqrt() {

    assert(this.n > 0, 'ds-math-sqrt-of-zero');

    let x: number = 0;
    let y: number = this.n;
    let z: number = 0;

    if (y > 3) {
      z = y;
      this.n = z;
      x = y / 2 + 1;
      while (x < z) {
        z = x;
        this.n = z
        x = (y / x + x) / 2;
      }
    } else if (y != 0) {
      z = 1;
      this.n = z
    }
    return this;
  }

  public print() {
    console.log('=', this.n);
    return this;
  }

}

// Here it is in action:

new SafeMath(0).add(10).print().sub(1).print().mul(2).print().div(3).print().pow(4).print().sqrt().print();

Output:

= 10
= 9
= 18
= 6
= 1296
= 36
suchislife
  • 4,251
  • 10
  • 47
  • 78