P5.js: Feature request: fract()

Created on 30 Jan 2019  路  36Comments  路  Source: processing/p5.js

Nature of issue?

  • [ ] Found a bug
  • [ ] Existing feature enhancement
  • [x] New feature request

Most appropriate sub-area of p5.js?

  • [ ] Color
  • [ ] Core/Environment/Rendering
  • [ ] Data
  • [ ] Events
  • [ ] Image
  • [ ] IO
  • [x] Math
  • [ ] Typography
  • [ ] Utilities
  • [ ] WebGL
  • [ ] Other (specify if possible)

New feature details:

I think that having a fract() function to return the fractional part of a number might be a nice addition to the math functions. I was reading through the book of shaders and it was showing how fract() is used in generating noise and I thought it would be a cool function to have as a part p5js.

I get that if someone wanted this function they could easily write it themselves as its just returning what is passed as a paramater minus the floor of that value, but I feel like it fits in with the rest of the math functions.

Anyway, let me know what you think about adding fract().

Thanks!

math feature request

Most helpful comment

Hello !
If I may add one suggestion ,
I think for negative numbers , we must return 1 minus the fractional part of the number,
For eg :
fract(1.3) = 0.3
fract(-1.3) = 0.7
The current suggested code returns 0.3 for both cases. Hopefully , it's of help !

All 36 comments

It's not really needed as (n%1).toFixed(4) works well.

https://stackoverflow.com/a/4512317/45966

It's not really needed as (n%1).toFixed(4) works well.

https://stackoverflow.com/a/4512317/45966

I recognize that there are easy ways for people to implement fract() on their own, but there other functions in p5js that are just as easy to implement. You could argue that a function like sq() isn't really needed because it just returns the value passed through multiplied by itself.

If this feature is to be added, I would like to do it.

@ScottGrogin Is this issue still open? I would like to work on this asap if possible. Please let me know which file to change. Thank you

What is the proposed implementation? Because with something like (n%1).toFixed(4) although practically it probably would not cause much problem, you are limited in terms of the number of precision after the decimal point, not to mention that toFixed returns a string and not a number (so does it belongs with String Functions such as nf or Math Functions such as floor?) Simply doing n%1 would cause floating point precision issue which is why the stackoverflow solution have the toFixed portion.

@vedhant @ankur54 If any of you are interested in looking into implementing this, I would suggest fleshing out what your proposed implementation would be in this issue first before heading straight into the source code. Otherwise the likelyhood of said PR is rejected is higher when you did not have a clear idea about the implementation that the community has looked at, because there could very likely be edge cases that you didn't account for that we would have otherwise spotted and fixed while in the discussion phases here on the issue.

toFixed belongs to Number native, and does return a string.

you are limited in terms of the number of precision after the decimal point

Totally agree

Ok, so my initial idea was to use (n%1).toFixed(4) and then convert it back to number use parseInt(x, 10). So, the overall function would look something like this:
parseInt((n%1).toFixed(4).slice(2), 10)

Although, I could not find any corner cases, if the above function does not satisfy, we can write a function that does something similar to long division method, i.e., takes the remainder of the number, multiplies it by 10 divides it, take the quotient again takes the remainder and repeat all over again until required precision.

How does both of the idea look like?

@ankur54 parseInt((n%1).toFixed(4).slice(2), 10) would not work because if you make n = 1.0004 for example, it returns 4. Something like parseFloat((n % 1).toFixed(4), 10) would have worked better (why slice the number?) However, it still doesn't solve the problem of lost precision (parseFloat((1.00004 % 1).toFixed(4).slice(2), 10) will simply return 0 instead of 0.00004)

takes the remainder of the number, multiplies it by 10 divides it, take the quotient again takes the remainder and repeat all over again until required precision.

I don't quite understand this can you maybe explain with a simple example?

n = 1.0004

Oh, shoot!!! I forgot that case. Maybe you can give me some examples, to better understand what kind of output you would expect. Also, in case of your example, what do you expect output to be? Is it 0004 or .0004? If its the former one, can you show pending zeros, before a number in case of an int data type?

I don't quite understand this can you maybe explain with a simple example?

Maybe the code would be a clearer way to explain. But before that can you please clear my doubts?

Can we also consider the results for the input values 1e20 and 1e-20?

I'm not sure what to expect which is kinda the point to the discussion.

Basically: Should the function return a string or a number? If it is to return a number, it has to return 0.0004 in the case of n = 1.0004 because there is only a single number type in Javascript and that is a 64-bit floating point number. If it is to return a string then we can ask if we return the decimal point or not.

I can't really give an exhaustive list of example or rather I can't be sure that I have given such a list of examples that is complete, at least not without solving the above problem first. However, you can consider below some numbers that may be passed to the function:

0.1
1.5
1.00004
0.00005
1e20 // as suggested by Spongman
1e-20 // as suggested by Spongman
5
0.3-0.2 // in js it evaluates to 0.09999999999999998
// there probably are many more
function scientificToDecimal(num) {
    //if the number is in scientific notation remove it
    if(/\d+\.?\d*e[\+\-]*\d+/i.test(num)) {
        var zero = '0';
            parts = String(num).toLowerCase().split('e'), //split into coeff and exponent
            e = parts.pop(),//store the exponential part
            l = Math.abs(e), //get the number of zeros
            sign = e/l,
            sgn = parts[0][0],
            coeff_array = parts[0].split('.');
        if(sign === -1) {
            coeff_array[0] = Math.abs(coeff_array[0]);
            num = sgn + zero + '.' + new Array(l).join(zero) + coeff_array.join('');
        }
        else {
            var dec = coeff_array[1];
            if(dec) l = l - dec.length;
            num = sgn + coeff_array.join('') + new Array(l+1).join(zero);
        }
    }

    return num;
};

num = parseInt(n, 10)
n = scientificToDecimal(n);

if(n > num)
console.log(n - num)
else 
console.log(num - n)

How about this?

@limzykenneth Please comment on the code.

@ankur54 Have you tried some examples with the code above? What do they return? Honestly, most of this is not a matter of how to implement the feature in code but rather to determine what the code should be returning when provided with numbers that can be interpreted in different ways (mainly because of the floating point nature of Javascript Number).

PS. There's no need to ping me on a separate comment, I (and others as well) may not always be available to comment immediately but we will get notifications whenever you post a new comment on an issue we are already participating in.

I'm going to have a think about this, if anyone else have an idea do let us know.

I have checked it with a lot of examples, for which, it returns the correct answer. Although the only issue here is, it returns a String instead of a floating point, which I prefer, because, in cases of large floating point numbers, parseFloat() changes it to scientific representation.
Also, sorry for explicitly pinging you.

@ankur54 Running the code you provided and setting n = 1e-20 returns 9.

@ScottGrogin In your case, are you thinking of getting the fractional digits as a string or as a number?

I have tweaked the code a little bit.

function scientificToDecimal(num) {
    //if the number is in scientific notation remove it
    if(/\d+\.?\d*e[\+\-]*\d+/i.test(num)) {
        var zero = '0';
            parts = String(num).toLowerCase().split('e'), //split into coeff and exponent
            e = parts.pop(),//store the exponential part
            l = Math.abs(e), //get the number of zeros
            sign = e/l,
            sgn = parts[0][0],
            coeff_array = parts[0].split('.');
        if(sign === -1) {
            coeff_array[0] = Math.abs(coeff_array[0]);
            num = sgn + zero + '.' + new Array(l).join(zero) + coeff_array.join('');
        }
        else {
            var dec = coeff_array[1];
            if(dec) l = l - dec.length;
            num = sgn + coeff_array.join('') + new Array(l+1).join(zero);
        }
    }

    return num;
};

n = 126.8232325346e40;
n = scientificToDecimal(n);
num = (typeof n == "string") ? n.split('.')[1] : n.toString().split('.')[1];
if(num == undefined) num = "0000";

console.log('.' + num)

I am sorry if my solution is not up to the mark, but it seems you are looking for something different as a solution. It would be helpful if you could tell me.

@ankur54 No don't worry, actually this is much better. Its behaviour seems to be pretty consistent.

I'm not looking for anything in particular other than an idea about how to deal with unexpected numbers, not really in terms of code (although it can be), but rather given any particular input what the output should be. Which means answering questions like "should the function return a string or a number", "how should the function handle scientific notated numbers", "how should the function handle cases where there are lost of precision due to floating point arithmetic", etc.

There isn't a right or wrong way of answering those questions as they can be dealt with one way or another, for example, the function can return a string or a number and either will be fine but it is a matter of which is the preferred solution. I haven't find myself looking to use this functionality so I can't say one way or the other is better so that's why I need input from other people including you to give an opinion on what is preferred.

@ankur54 Running the code you provided and setting n = 1e-20 returns 9.

@ScottGrogin In your case, are you thinking of getting the fractional digits as a string or as a number?

Definitely thinking that it should be number and not a string.

Hello !
If I may add one suggestion ,
I think for negative numbers , we must return 1 minus the fractional part of the number,
For eg :
fract(1.3) = 0.3
fract(-1.3) = 0.7
The current suggested code returns 0.3 for both cases. Hopefully , it's of help !

@limzykenneth Hey, if there's still a problem over this implementation, can I take it up? (asking if there's a problem because there isn't any reply to @Ajayneethikannan 's suggestion)

Also, if there's still a problem, my proposed implementation would be somewhere along the lines of using the concept of Significant Digits for the numbers to take care of leading zeroes and keeping a count for decimal precision (using strings and not numbers for now for maximum precision, although there might be some lack of efficiency that numbers may provide).

@dhruvs009 Sorry this issue is a bit old and I can't recall the discussions we had until that point. If you can give an outline of the behaviour you envision this function to have then we can continue the discussion and later a PR.

@limzykenneth Hey, sorry, I missed your reply yesterday. So basically what I was suggesting is that I'd work on the number (whose fractional part needs to be returned) as a string and read the significant digits it has, basically solving the problem in scientific notations. Also, I would suggest passing an additional parameter, to let the user decide the amount of precision he needs.

@limzykenneth Keep in mind that passing in an algebraic expression would need more work, but I think it would be easier to solve using the concept of significant digits and it's various rules (as for an example you gave (0.3-0.2))

I think as mentioned above, we would prefer to return numbers instead of strings. For something like 0.3-0.2 it is pretty much expected to have the expression evaluate before passing it to any functions it needs to go through, even if it returns 0.09999999999999998.

@limzykenneth Okay, so just to be clear, is it okay if any expression passed into fract() returns inaccurate values as in 0.3-0.2? Or is fract(0.3-0.2) supposed to return 0.1? Because if it returns 0.0999... then that is how the language works and there wouldn't really be anything we could do as far as I can think of? I think one possible solution would be to round it off to the nearest number that has the same amount of significant digits as the expression is supposed to have?

For me that is acceptable because Number is a floating point type and not a decimal type so this kind of precision issue will exist and in most cases won't be a problem.

@limzykenneth Umm so a solution around the lines of what @ankur54 has suggested would work?

It might, just depends on the use case, the potentially trickiest ones are the ones with scientific notation and how to handle negative numbers. Again this is a matter of what the use case is and not that there is one right implementation so I don't have the answer here.

Okay, I'll post a possible implementation here tonight, so just maybe have a look at it when you can.

function fract(num){
    var toConvert=Number(num);
    var sign=0;
    if(isNaN(toConvert)){
        return NaN;
    }
    else{
        if(toConvert<0){
            toConvert=-toConvert;
            sign=1;
        }
        if(toConvert==Infinity){
            return toConvert;
        }
        else if(String(toConvert).includes('e') && String(toConvert)[String(toConvert).indexOf('e')+1]==='-'){
            return Math.abs(sign-toConvert);
        }
        else if(String(toConvert).includes('.')){
            toConvert=String(toConvert);
            toConvert="0"+toConvert.slice(toConvert.indexOf('.'));
            toConvert=sign-Number(toConvert);
            return Math.abs(toConvert);
        }
        else{
            return 0;
        }
    }
}

Okay, so what I noticed was that any exponential of the form Xe+Y is always an integer in JavaScript as it is scaled down to the number of digits it is accurate to (15 digits), the decimal is moved to a position such that there is only one digit to the left of it (as I'm sure you are aware that is how scientific notation works), and then the remaining exponent is given in scientific form if the language doesn't allow basic appending of zeroes, otherwise it is just converted to a normal Number (i..e, not in scientific notation) so my code just returns 0 for such inputs.

For exponentials of the form Xe-Y, it is always a number between 0 and 1 as it is also worked upon in a way similar to the above. These exponentials are converted to normal decimal notation (i.e, not in scientific notation) after being scaled to fit in the allowed number of digits (15-18 digits in total including those before and after the decimal point) and the exponent is written in a similar way as I mentioned above. So, my code just returns Xe-Y for such inputs.

For other numbers, I think the code is easy to understand? If there's anything you didn't understand I'll be happy to explain.

Also, I noticed there's a feature of referring to Infinity as a Number too. I didn't really know what to return there so I returned Infinity (I don't think this case really matters as for all practical purposes I'm pretty sure you wouldn't be needing to evaluate fract(Infinity), but yes you need to handle all possible cases so I've included it as well).

P.S: I'm really sorry if the explanation is a bit hard to understand. This was the easiest language I could come up with.

Hey, @limzykenneth, have you had a chance to look at this?

For me that seems to be ok. You can go ahead and file a PR for this and let's see if there's any implementation details that needs iron out there. I would like to see some opinions from other contributors before we merge this as well.

Okay, I'll file one today

@limzykenneth this is finally closed by #4069, isn't it?

Yep thanks @dhruvs009 馃巿

Was this page helpful?
0 / 5 - 0 ratings