Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

modulo function #21

Open
Louis-Aime opened this issue Feb 5, 2021 · 8 comments
Open

modulo function #21

Louis-Aime opened this issue Feb 5, 2021 · 8 comments

Comments

@Louis-Aime
Copy link

I'd like to add a modulo function, different from standard %.
Let's call it Math.mod (x, d).

  • if either x or d is not a number, return NaN.;
  • if (d= 0) return NaN;
  • if (x = 0) return 0;
  • if (d > 0) then
    • if (x > 0) {return x % d }
    • else /* that is, if (x < 0) */ return (x % d) + d;
  • else /* that is, if d < 0 */ return Math.mod (- x, -d);

Motivation: operator % produces the remainder of x diveded by Math.abs(d). There is no function that yields the algebraic modulo of x by d, which is of the same sign as d.
Not having this function handy causes frequent bugs in calendrical computations, including ICUs.

@Andrew-Cottrell
Copy link

Andrew-Cottrell commented Feb 6, 2021

The following implementation, of the suggested function, appears to satisfy the criteria described above.

/**
 * A modulo operation that finds the remainder after division of a dividend
 * by divisor, using Donald E. Knuth's floored division.
 * 
 * The remainder will have an absolute value less than the absolute
 * value of the divisor. Unlike the ECMAScript remainder (%)
 * operator, the remainder will have the same sign as the divisor.
 * 
 * @param {number|BigInt} dividend - A finite number.
 * @param {number|BigInt} divisor - A non-zero number.
 * @return {number|BigInt} The remainder.
 * @throws {TypeError} If exactly one of the parameters is a BigInt.
 * @throws {RangeError} If the divisor is 0n.
 */
Math.mod = function ( dividend, divisor ) {
    /* based on https://github.com/google/closure-library/blob/v20210106/closure/goog/math/math.js#L68 */
    var remainder = dividend % divisor;
    if ( remainder * divisor < 0 ) {
        remainder += divisor;
    }
    return remainder;
};

/* if either dividend or divisor is not a number, return NaN */
Math.mod( "a", 1 );        // NaN
Math.mod( 1, "a" );        // NaN
Math.mod( NaN, 1 );        // NaN
Math.mod( 1, NaN );        // NaN

/* if divisor is a zero, return NaN or throw RangeError */
Math.mod( 1, 0 );          // NaN
Math.mod( 1, -0 );         // NaN
Math.mod( 1n, 0n );        // RangeError: Division by zero

/* if dividend is a zero, return the dividend */
Math.mod( 0, 1 );          // 0
Math.mod( -0, 1 );         // -0
Math.mod( 0, Infinity );   // 0
Math.mod( 0, -Infinity );  // 0
Math.mod( -0, Infinity );  // -0
Math.mod( -0, -Infinity ); // -0
Math.mod( 0n, 1n );        // 0n

/* if the dividend is an infinity, return NaN */
Math.mod( Infinity, 1 );   // NaN
Math.mod( -Infinity, 1 );  // NaN

/* if the divisor is an infinity with the same sign as the dividend, return the dividend */
Math.mod( 1, Infinity );   // 1
Math.mod( -1, -Infinity ); // -1

/* if the divisor is an infinity with the opposite sign as the dividend, return the divisor */
Math.mod( -1, Infinity );  // Infinity
Math.mod( 1, -Infinity );  // -Infinity

/* integers */
Math.mod( 4, 3 );          // 1
Math.mod( 4n, 3n );        // 1n
Math.mod( 4, -3 );         // -2
Math.mod( 4n, -3n );       // -2n
Math.mod( -4, 3 );         // 2
Math.mod( -4n, 3n );       // 2n
Math.mod( -4, -3 );        // -1
Math.mod( -4n, -3n );      // -1n

/* non-integers */
Math.mod( 5.5, 2 );        // 1.5
Math.mod( 5.5, -2 );       // -0.5
Math.mod( -5.5, 2 );       // 0.5
Math.mod( -5.5, -2 );      // -1.5

Except corner cases, this is the same as

@Rudxain
Copy link

Rudxain commented Apr 29, 2022

My concern is that Math.mod may be an ambiguous name, and there's multiple valid definitions of mod, and there's even another definition not mentioned there which makes use of roundInf expand (rounding away from zero, and towards unsigned Infinity).

Even if none of those definitions are ever added (specially modCeil which is useless compared to modFloor), we should be aware of them, because maybe someday modEuclid gets added.

And we have another problem (only if more than 2 definitions are added): Should all be merged into 1 mod method with a 3rd string argument to specify the variant? Or should they all be splitted into standalone methods?

Yet another problem is modPow: should modPow only use 1 variant? Should it have a 4th arg to specify the variant? Or should it be split in multiple methods?

@Andrew-Cottrell
Copy link

Andrew-Cottrell commented Apr 29, 2022

My concern is that Math.mod may be an ambiguous name...

In my own library, I named it floorMod (following Java's java.lang.Math.floorMod) to avoid ambiguity by clarifying the rounding mode. However, given Microsoft Excel and LibreOffice Calc use MOD, it is reasonable to use mod to mean the floored modulo. Something like modEuclid could be added, concurrently or later, with a longer name for — what I assume would be — less common use cases.

@Rudxain
Copy link

Rudxain commented Apr 29, 2022

I agree, seems reasonable considering the "most" standard mod definition (in mathematics) is defined using floor division

@Rudxain
Copy link

Rudxain commented May 19, 2022

Today I learned that what I call "roundInf" is actually named expand in the official and standard Intl.NumberFormat API. Here (at the roundingMode option). In Java, that same rounding mode is named UP.

I'll open a new Issue about rounding modes.
Update: #28

@vimirage
Copy link

vimirage commented Jul 4, 2022

I'd like to add a modulo function, different from standard %. Let's call it Math.mod (x, d).

* if either x or d is not a number, return NaN.;

...

Note that these functions would just coerce their arguments to numbers instead.

@Crissov
Copy link

Crissov commented Aug 24, 2023

I’ve frequently had the need for a non-zero modulo function, where zmod(a*b, b) returns b instead of 0, e.g. when calculating ordinal indices starting with 1 as in dates. This is easily done with mod(a*b - 1, b) + 1, except I’m not sure this always behaves as expected for parameters being negative or zero.

PS: Apparently, Mathematica has a generalized modulo function with variable offset d for this: Mod[a, n, d].

Math.mod = function ( dividend, divisor, offset = 0 ) {
    var remainder = (dividend - offset) % divisor + offset;
    if ( remainder * divisor < 0 ) {
        remainder += divisor;/*?*/
    }
    return remainder;
};

(Naively adapted from the code @Andrew-Cottrell had posted.)

@ljharb
Copy link

ljharb commented Aug 24, 2023

can't you maybeZero || b in that case?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants