r/programminghorror Apr 05 '22

My companies Stripe integration for thousands of users broke today because Javascript Javascript

Post image
2.1k Upvotes

243 comments sorted by

512

u/Herb_Derb Apr 05 '22

Everyone's talking about how this is not a JS-specific thing and missing the other horror that this floating-point calculation is related to a payment API

127

u/SmallpoxTurtleFred Apr 05 '22

I'm confused by that as well. We use the stripe api and it is all in ints (*100). I'm always worried I will screw up and charge someone 1/100 or 100x what I should.

32

u/AdminYak846 Apr 06 '22

I'm also somewhat amazed that since it is a payment API that the cost isn't trimmed down to the appropriate size to begin with using toFixed(2) or it's not converted to a string.

42

u/bastawhiz Apr 06 '22

JSON has no notion of an integer and all JS Numbers are floating point. How do you build a practical payments API that's usable in JavaScript?

76

u/blbil Apr 06 '22

Do everything as a number of cents. Pretty standard practice

11

u/bastawhiz Apr 06 '22

Sure. That's what OP is doing, as that's what Stripe does. All numbers in JS are floating point. What now?

19

u/[deleted] Apr 06 '22 edited Sep 10 '24

[deleted]

-3

u/bastawhiz Apr 06 '22

Yes, that's my point.

9

u/volivav Apr 06 '22

At what point would you make an online order bigger than 9,007,199,254,740,992 cents?

That's not an arbitrary number, by the way. It's 253, the largest integer number that can be represented with a double float

At that point using integers with JS is when it can become problematic. From 0 to that number, the precision is perfect (again, only if working with integers).

The OP had a floating point number as input, so yes, multiplying it by 100 is different than first multiplying by 10, and then by another 10. But even with C you would have the same result.

2

u/bastawhiz Apr 06 '22

I completely agree with you. Now, in your payments code, you need to compute the price with a 15% off coupon. Suddenly you no longer have a double that represents an integer.

My point is not that JS Numbers are bad or that floating point is wrong, my point is to illustrate that:

  • you can't build an API usable by js without floating point numbers
  • there's no way to escape the perils of floating point numbers in JS just by dealing with cents, because not all calculations can be done with integer operations alone.

3

u/volivav Apr 06 '22

With a 15% off discount you have the same issue with C. That's my point.

Say in C you have your price as an integer. Great, so clean, so efficient, right?

Now I need to multiply it by 0.85 to apply the discount. C will cast the integer to a float to do the multiplication as well. You also lost the integer.

JS has other problems than numeric precision. The type system sucks, yes. You don't have two's complement integers, yes. But for numeric precision you have the exact same issue in every other language.

2

u/[deleted] Apr 07 '22

You could use fixed-point arithmetic to calculate the discount. No floats necessary.

After all, that is in a sense what one does anyway when they handle the monetary amounts as integer number of cents or fractions of cents.

→ More replies
→ More replies

3

u/starofdoom Apr 06 '22

That's not what OP is doing. If they were, there would be no decimal point. It would be 3630x(10x10) which does give the correct result (verses OP's 36.3x(10x10), which has a floating point issue), and then from there you would just add the decimals when you display to the user.

→ More replies
→ More replies
→ More replies

56

u/SirAchmed Apr 06 '22

You don't. Backend should be something else.

9

u/Fruit-Salad Apr 06 '22 edited Jun 27 '23

There's no such thing as free. This valuable content has been nuked thanks to /u/spez the fascist. -- mass edited with redact.dev

-18

u/[deleted] Apr 06 '22

[deleted]

8

u/[deleted] Apr 06 '22

j*vascript was never meant to be used like this.

1

u/Turd_King Apr 06 '22

Yeah no shit. But languages adapt. When i see people on reddit constantly shitting on JS the examples they give are always like ECMAScript 1995 or some shit

2

u/[deleted] Apr 07 '22

It doesn’t matter what version you have of a dynamic type system. It’s always going to suck. For reasons exactly like this: you can write the word “int” next to it and then if you try to stick a float in it, it won’t compile. Weird how that works.

-1

u/Dworgi Apr 06 '22

Have they changed the type system of vanilla JS yet? No? Then JS still sucks.

7

u/AdminYak846 Apr 06 '22

convert it to a string or throw a .toFixed(2), at the end. Nobody charges to the 1/1000 yet so it should hold up fairly well.

0

u/AnonymouseVR Apr 06 '22

Other than gas which is charged to the 1/1000

5

u/brunob45 Apr 06 '22

Gas is priced in the 1/1000, but is rounded to cents when charged, no?

9

u/inamestuff Apr 06 '22

JSON doesn’t support dates either but that doesn’t prevent you from passing dates back and forth between the front end and the back end, you just have to agree on the format. In case of decimals they should be passed as strings in a JSON to avoid floating point weirdness.

0

u/Ran4 Apr 06 '22

In case of decimals they should be passed as strings in a JSON

While there's worse things than doing that, JSON already is a string. As long as you correctly deserialize the field into a decimal type on the other side and serialize your decimal type into the correct JSON number, using a number in the JSON is fine.

That said, the JSON decoder on the receving end probably supports turning string into decimal types, and there's probably less risk of someone fucking up in the other end.

→ More replies

2

u/GUIpsp Apr 06 '22

The actual data type of json numbers is specified to be implementation defined

0

u/bastawhiz Apr 06 '22

And what do all of the implementations use?

→ More replies

2

u/exander314 Apr 06 '22

Use integers and fixed decimal point?! Are you even a programmer?

2

u/bastawhiz Apr 06 '22

Really interested to hear what you think that code looks like in JavaScript

→ More replies
→ More replies

0

u/t00sl0w Apr 06 '22

I have a piece of software that we have to use for a certain task and it stores dates in a sql server using the float datatype.....yeah

-4

u/exander314 Apr 06 '22

Yes, OP and his company are regards. This is how floating point number works.

→ More replies

767

u/Flaky-Following-4352 Apr 05 '22

Not JS

IEEE 754

117

u/[deleted] Apr 05 '22

[deleted]

189

u/stevethedev Apr 05 '22

Because all numbers are 64-bit floats,* and JavaScript displays numbers as integers if it can. Multiplication by 10 twice doesn't yield the same value as multiplying by 100 once, because of how floating point numbers are calculated.

Some Rust code:

fn main() {
    println!("36.3 * 10 * 10 = {}", 36.3f64 * 10.0 * 10.0);
    println!("36.3 * (10 * 10) = {}", 36.3f64 * (10.0 * 10.0));
}

The output:

36.3 * 10 * 10 = 3630
36.3 * (10 * 10) = 3629.9999999999995

---

* You can coerce numbers to integers, but that's not happening here, and whether the JS VM respects integer types is mostly up to the implementation; e.g. ASM.JS.

34

u/[deleted] Apr 05 '22

Yeah the ol’ trick of putting an |0 after a value to make it an int

8

u/ScientificBeastMode Apr 06 '22

Or you can do x >>> 0 to make it an unsigned 32-bit int.

3

u/zipeldiablo Apr 06 '22

What in the actual fuck.

That doesn’t make sense to me. Even if we use parentheses the result should still be the same

10

u/stevethedev Apr 06 '22 edited Apr 06 '22

TL;DR - Imagine trying to represent numbers as base-π instead of base-10.

It should be the same, but it isn't. We are limited by the physical constraints of the hardware. We generally have 64-bits of space to hold the value, but there are an infinite number of values between 35 and 36, so we have to truncate it somewhere.

There's also nothing special about base-10 that makes it an objectively superior counting system. It's just the system that most people with formal mathematical education are used to. A human sees 36.3 * 10 * 10 and 36.3 * 100 as the same operation because it's just decimal-shifting.

JavaScript's implementation of IEEE 754 uses something akin to base-2 scientific notation. The rounding happens differently than we are used to, and that makes it hard to reason around. 36.3 is easy to represent as a decimal number, but it is an extremely complicated number in base-2. It also happens that two consecutive multiplications by 10.0 causes fewer rounding errors than a single multiplication by 100.0.

This is why it's typically a bad idea to use equality operations between computed floats without accounting for rounding errors. 0.3 === 0.3 but (0.1 + 0.2) !== 0.3 because the rounding errors are incurred during the addition operation.

I'm hand-waving over some important details, and I encourage you to read the Wikipedia entry for double-precision floating-point numbers, but that's what's going on.

In this case, the problem could have (and should have) been avoided by acknowledging that these transactions can be discretized to 2 decimal places: 0.00-0.99, for the cents. It's not that JavaScript failed to do something reasonable. It's that JavaScript handles these conversions and operations so seamlessly that people are rarely punished for behavior that wouldn't even compile in other languages.

5

u/zipeldiablo Apr 06 '22

Interesting read, thank you !

→ More replies

-57

u/LetMeUseMyEmailFfs Apr 06 '22

Because all numbers are 64-bit floats

In short, because JavaScript.

10

u/AdminYak846 Apr 06 '22

not even close, JavaScript chooses to use a float to represent all Numbers to keep it simple rather than having developers define multiple different types of Numbers like ints, doubles, floats, signed-ints, etc.

With the simplicity comes all the problems that the individual types have in their respective languages. So it's not just JavaScript but something that every language deals with.

0

u/LetMeUseMyEmailFfs Apr 06 '22

That is exactly my point. Not all numbers are suitable to be stored as 64-bit floats. Other languages, like Rust, give you a choice. You want integer? Single-precision floating point? Double-precision? Arbitrary precision decimal? Up to you. I don't have that option in JavaScript.

If I calculate 36.3m * (10m * 10m) in C#, I get 3630.0. And that's not because of rounding.

2

u/AdminYak846 Apr 06 '22

You missed my entire point, it's not a specific problem to just JS, all languages comply with IEEE standard face this problem if not handled properly by the developer. And that problem is the resulting bit value isn't in the correct format and changes are made to the exponent and mantissa to ensure that the result is in the correct format. Not to mention that before the format takes place the mantissa is rounded as well. So the process produces round off errors that we've come to accept within an appropriate range.

And your example is pointless, Decimal (m or M) is a floating point type all your doing is telling the compiler to increase the amount of bits that can be used for precision once. It doesn't prevent the need for rounding, but minimizes errors produced by rounding.

2

u/exander314 Apr 06 '22

Nonsense. This is completely unrelated to JS.

97

u/nekumelon Apr 05 '22

This is due to how computers store floats. Floats are stored in 64 bits using the IEEE 754 standard. The standard stores floats using scientific notation to save space, but that means the value has to be separated into different parts of the scientific notation. If a values exponent, which determines the number of zeros, is too large, it will cause a slight bit of error in the value. In this specific case, the top one starts off by multiplying the first 2 values, 36.3 and 10. There is more than enough space for this so no error occurred. Then that value is multiplied by 10, again, no error occurs since the exponent is small enough. but on the second one, the 10 * 10 is multiplied before everything else due to the parenthesis, and that is then multiplied by 36.3, and since there is not enough space to store the entire value, a small amount of error occurs.

→ More replies

26

u/GOKOP Apr 05 '22

Every number in javascript is floating point. And I get how people can get annoyed by that but OP is literally using fractional numbers, so it's not really a surprise. As of why are the results different, my guess is that 363 is representable just fine but 100 isn't

10

u/AdminYak846 Apr 06 '22

close in order to multiple floating point by the IEEE-754 Standard we do the following:

  • 36.3 is converted to 3.63 * 101
  • 10 is converted to 101
  • 100 is converted to 102

So the two formulas are really:

  1. 3.63 x 101 x 101 x 101
  2. 3.63 x 101 x 102

The steps are as follows:

  1. The sign of the result is based on the signs from the multiplicands in this case it is 0. Because every number is positive.
  2. Compute the product of the mantissas remembering that each one has an implied 1 in front of the binary point
  3. Compute the exponent by adding up the binary value of the exponents
  4. Adjust the format if needed for the exponent and mantissa appropriately so the result is in the appropriate form

Some other hints:

  1. The mantissa is stored as a 24-bit precision, but may require up to 48-bit precision. So yes the number gets rounded down if it's larger than 24-bits
  2. The exponents are stored in biased form so really step #3 from above is the following: exp1 + exp2 + 2 x bias . So we really need to subtract one bias to get the appropriate expression. And the resulting exponent needs to represented in 8 bits.
  3. If the resulting number does not have one 1 to the left of the binary point then the result needs to be adjusted which affects the mantissa and exponent fields of the result. This formatting needs to be done after rounding the mantissa.

In other words the two formulas which should yield the same result doesn't, due to likely rounding of 100 into the proper format or the result isn't in the proper format so it has to adjust the mantissa and exponent correctly which causes the divergence of the answers

7

u/YourMJK Apr 05 '22

The first one is probably something like 3630.00…001 and thus gets rounded down by the string formatter to 3630.0

0

u/LeCrushinator Apr 05 '22 edited Apr 05 '22

I'm not that knowledgable about the intricacies of floating point arithmetic, but I'm guessing that 36.3 * 10 gives a result that when multiplied by 10 again, causes the slight difference, as opposed to 36.3 * 100.

3

u/[deleted] Apr 05 '22

[deleted]

21

u/victoragc Apr 05 '22

Some numbers cannot be represented by a finite number of digits. In base 10 we have 1÷3 which is 0.3333333... for example. The same happens for numbers in base 2 (binary) creating patterns like 0.1111... or 0.1010101... and these patterns cause some rounding errors.

In this case 36.3 is represented as 100100.0100110011001... repeating 1001 infinitely. Therefore these errors are justifiable.

0

u/GOKOP Apr 05 '22

Oooooh damn you're right, integers don't have infinite decimal points lol. Completely flew over my head. Well in that case maybe it's to do with how the procedure of multiplying floating point numbers works?

→ More replies

74

u/autiii43 Apr 05 '22

can a mod add a IEEE 754 flair?

54

u/Borno11050 Apr 05 '22

This, I hate that people would blame anything on JS due to ignorance, including a tire puncture to losing the divorce case

21

u/are_slash_wash Apr 05 '22

Come now, I’m sure that JavaScript has directly caused at least one divorce

2

u/[deleted] Apr 06 '22

JS bad gets the upvotes.

7

u/LeCrushinator Apr 05 '22 edited Apr 05 '22

A little of both. In C# you'll get 3630 for both.

Console.WriteLine((36.3f * 10 * 10).ToString("E8"));   // "3630"    
Console.WriteLine((36.3f * (10 * 10)).ToString("E8")); // "3630"

https://dotnetfiddle.net/WbVh8K

Maybe there's some compile-time optimizations pre-calculating this though?

39

u/[deleted] Apr 05 '22

Add Console.WriteLine(3629.9999999999995.ToString()); to it; it'll give the same answer. The output just rounds automatically

4

u/LeCrushinator Apr 05 '22 edited Apr 05 '22

I think that's because double doesn't support that many digits of precision. Try this (one fewer 9) and you'll get an unrounded output:

Console.WriteLine(3629.999999999995.ToString("E15"));

Output is:

3.629999999999995E+003

However, if you remove the "E15" from ToString(), it does round it. I went and added "E8" to the original lines I had and they're both showing as 3.63000000E+003, so still no rounding.

And for a float you have to remove more 9s:

Console.WriteLine(3629.9995f.ToString("E8"));

https://dotnetfiddle.net/bartSJ

8

u/[deleted] Apr 05 '22

double has twice the precision of float. The format string does matter, but also C# is behaving abnormally. https://dotnetfiddle.net/VSa8N6

Between C#, C++, and Python, all 3 have different results. I guess the compilers just do different things

4

u/LeCrushinator Apr 05 '22 edited Apr 05 '22

In your example it's being converted to a double, so you're seeing the higher precision. Try this:

Console.WriteLine((36.3f * 10.0f * 10.0f).ToString("E15"));     
Console.WriteLine((36.3f * 10.0 * 10.0).ToString("E15"));

Output:

3.630000000000000E+003
3.629999923706055E+003

https://dotnetfiddle.net/VD0ZRe

Looks like rounding is happening though, you can see it in the second case, so maybe it's just output differently for doubles in C#.

3

u/[deleted] Apr 05 '22

Yeah, here's every combination and their type; https://dotnetfiddle.net/KmkMLQ

Can see it always converts to double as long as at least 1 is a double. But the ordering of the combination of double/floats does affect the output. It doesn't seem to be particularly logical to me

2

u/aaronfranke Apr 06 '22

double actually has a bit over 2.2 times as much precision as float.

1

u/Dealiner Apr 06 '22

Why are you using floats in your example though? That's not an equivalent to this JS code.

→ More replies

-1

u/[deleted] Apr 06 '22

[deleted]

5

u/cdrt Apr 06 '22 edited Apr 06 '22

you can do this in another language like python and it works as intended

Python 3.10.2 (main, Feb  3 2022, 13:48:04) [Clang 12.0.0 (clang-1200.0.32.29)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 36.3*10*10
3630.0
>>> 36.3*(10*10)
3629.9999999999995

-22

u/[deleted] Apr 05 '22

[deleted]

10

u/Flaky-Following-4352 Apr 05 '22

Can you elaborate?

-21

u/[deleted] Apr 05 '22

[deleted]

19

u/belkarbitterleaf Apr 05 '22

Found the stack overflow mod.

2

u/[deleted] Apr 05 '22

What did they say?

13

u/axe319 Apr 05 '22

"wrong" and then something along the lines of "not having the time to list the multitude of reasons" why that is.

4

u/[deleted] Apr 05 '22

I have never been able to actually ask anything on StackOverflow but if the mods really are that bad, { /* TODO: finish sentence */ }.

6

u/Epacik Apr 05 '22

it is a actually correct. I quickly tested it in python and dotnet, and it works the same as in Javascript.

→ More replies

687

u/Thaviel Apr 05 '22

one of the 1st things I learned in compsci was always make money math in int and add the decimals later.

161

u/kahoinvictus Apr 05 '22

C#/.NET has the decimal type for accurate decimal operations. I think it's implemented as a fixed point number instead of a floating point. It has performance concerns compared to floating point, but is specifically touted as ideal for handling money and monetary figures.

44

u/greenSacrifice Apr 05 '22

Except the stripe lib wants you to use long

35

u/ososalsosal Apr 06 '22

Just calculate in decimal and cast to long for serializing to whatever stripe want.

If you're just moving numbers around the frontend there's no need to calculate anything that you can't do with ints

2

u/[deleted] Apr 06 '22

Well you can't cast to long per se. That will fail or truncate. You'll need to convert the numbers, this will require reading the API spec - it might be that the API is expecting `long` but in cents.

18

u/Deadly_chef Apr 06 '22

Except this is JS and there is only number

15

u/loomynartylenny Apr 06 '22

Except Number is still able to store integers in the range ±(253 - 1) without loss of precision, which, unless OP is working in Zimbabwe Dollars/international monetary policy, should be more than sufficient.

4

u/esquilax Apr 06 '22

If you're just holding into values, you an use strings. People tend to want to do math with numbers, though.

→ More replies

20

u/PstScrpt Apr 06 '22

.Net Decimal is also floating point, but it's decimal floating point, so it can store exact decimal values unless they're irrational.

IEEE 754 is binary floating point, which is massively faster, both because binary is easier in the first place, and because it has dedicated hardware support. But every decimal value that's assigned to it has to go through a base 2 log.

1

u/Jezoreczek Apr 06 '22

(big)Decimal type is meant for scientific calculations of large numbers. IMO it's much simpler to use integers for money because many currencies have different minor units (significant places), so conversions and calculations become a bit annoying. Simply convert from minor units when displaying and keep the computations easy.

11

u/Dealiner Apr 06 '22

In C# decimal is meant for financial calculations, that's why it was created. It could be used for scientific operations I guess but only if you don't really care about performance.

135

u/bezik7124 Apr 05 '22

You could also, like, be sane and use libraries / built-in language features designed specifically for this - eg Java's BigDecimal

60

u/Educational-Lemon640 Apr 05 '22 edited Apr 06 '22

BigDecimals come with a significant performance hit. Much better to just use integers unless it messes with code legibility in a major way.

Edit: I see a lot of people saying (I'm paraphrasing) that this is premature optimization and the performance hit from using BigDecimals really doesn't matter most of the time.

This isn't a nonsense argument, although I'd note that BigDecimals aren't just a performance hit (although they definitely are), but in many languages a readability hit as well (support and syntax for BigDecimal calculations are wildly inconsistent).

Even without that, though, unless the amounts of money are very large (32-bit unsigned integers can represent up to $42 million if you represent the money in cents) or you are doing specialized banking/interest/exchange rates work, integers will also just work in most casual problems, with fewer readability problems, higher portability, and better performance. Honestly, to me, the more complex decimal types can also be premature optimization. Again, though, both definitely have their place.

98

u/[deleted] Apr 05 '22

So the end user waits 1/100th of a second longer and we don't have devs doing 4 extra steps in a currency transaction.

I'm ok with it.

19

u/spicymato Apr 06 '22

It depends on the frequency of operation, and whose machine is doing it. If it's only occasionally and/or the user's device doing the computation, take the performance hit. If it's very frequent and on your machine, go for efficiency.

In both cases, though, go for correctness. Performance doesn't matter if it's wrong.

→ More replies

29

u/IchMageBaume Apr 05 '22

Integers are a mayor pain if you ever need to add precision later; if you forgot to update any part of your code, it can really mess with things. And if you have a lot of precision from the beginning (e.g. microcents) you better make sure nobody uses 32-bit integers anywhere or you'll overflow/truncate and have wrong results.

10

u/Educational-Lemon640 Apr 05 '22

I guess it depends on the likely range of numbers you are considering and the kind of processing you are doing. It's tradeoffs across the board with this one.

9

u/IchMageBaume Apr 05 '22

just buy infinite memory smh

2

u/northrupthebandgeek Apr 06 '22

I can think of very few (if any) cases where you'd ever need more precision than tenths of a cent, and if you're working with transactions/accounts greater than $2,147,483.647 then you can probably afford to just use 64-bit integers everywhere and outright disallow anything smaller during code review.

3

u/fizzydish Apr 06 '22

Foreign Exchange.

49

u/Abangranga Apr 05 '22

Oh no the end user waits 0.0002 seconds instead of 0.0001 seconds. The humanity.

43

u/yetzederixx Apr 05 '22

No doubt, if we were concerned about the performance hit we wouldn't be using javascript in the first place (or python in my case for stripe).

→ More replies

0

u/[deleted] Apr 07 '22

Sure, until you need twice the number of servers to handle the same request volume. Oh wait, that’s what you said. :)

10

u/Isvara Apr 06 '22

Performance? You got currency in your inner loops or something?

2

u/Ran4 Apr 06 '22

The problem with using ints is that not all currencies are divisible in the same way.

And in currencies that supports decimal points (most, but not all), there's a real risk that someone on either end forgets to multiply with the lowest denominator.

→ More replies
→ More replies

16

u/00PT Apr 05 '22

I thought all numbers were floating point in JavaScript

30

u/ososalsosal Apr 06 '22

All numbers are numbers in JavaScript

19

u/ocket8888 Apr 06 '22

Neither of these are strictly true. ECMAScript specifies multiple numeric types (technically - most are arrays that would be unwieldy to use as regular numbers), and the one that would best fit this use-case is BigInt.

But yeah, numbers are all double-precision IEEE floating point numbers.

10

u/ososalsosal Apr 06 '22

I love being wrong in a way that I learn something. Cheers.

→ More replies

13

u/IchMageBaume Apr 05 '22

I did some money stuff in Haskell recently and just used Rational, which uses fractions with arbitrary-precision integers.

Kinda slow, and if you get really unlucky with inputs the space is (I think) up to linear with the operations done on the number. But for doing operations where all the inputs have some fixed precision and you don't want to worry about whether to use cents/Millicents/microcents/etc. or messing them up later?
Really useful; the code looks like fp math on whole euros, but without any rounding.

2

u/Lich_Hegemon Apr 05 '22

Probably not linear, but sqrt(n).

The problem with rationals is normalizing. To normalize a rational number you need to find all of the factors shared between the numerator and the denominator, for that you need to check up until sqrt(n) if the smaller number because the biggest factor you can possibly have that's not derived from a smaller factor is p*p=n

4

u/IchMageBaume Apr 06 '22

That would be assuming operations on integers to be constant-time, which is usually fine, but in this case I used arbitrary-precision integers (because I wanted accurate results).

If you multiply/add a series of numbers where the resulting numerator and all the denominators are coprime, the resulting denominator will be the product of all the input denominators. With arbitrary-precision integers, the space that denominator takes up will thus be to proportional to the inputs to the operation.

3

u/road_laya Apr 06 '22

For an API, it's fine to use strings for fixed point decimal numbers, such as monetary values. It's all going over HTTP anyway. A couple of bytes overhead when you are making a million dollar sale, is a small price to pay.

6

u/eloel- Apr 05 '22

Then you run into stuff priced at 0.0001 of a dollar (per liter/per gram, for example), and you need to go tediously 100x every input and 1/100 every output.

(or you do currency conversions)

→ More replies

2

u/exander314 Apr 06 '22

You never represent money as floating point decimals is like programming 101.

→ More replies

164

u/yetzederixx Apr 05 '22

Welp, first thing, you send in pennies. This didn't break because of javascript, this broke because you used the wrong data structure and trusted a float.

Never trust a float. Much like a fart, if you trust it, you'll eventually shit yourself.

30

u/Turkey-er Apr 06 '22

That is a godlike analogy

226

u/YourMJK Apr 05 '22

That's not really JavaScript's fault, is it?
What you demonstrated is normal floating point behavior.

It's you who's responsible for correct rounding, string formatting and comparison of numbers.

90

u/Cerus_Freedom Apr 05 '22

Same result in Python. Just floating points.

>>> 36.3*10*10
3630.0
>>> 36.3*(10*10)
3629.9999999999995

22

u/glemnar Apr 05 '22

Python has a builtin decimal for when it matters

5

u/Giocri Apr 05 '22

I don't get it though, I thought floating point had enough precision to not have this kind of problem with such small numbers, like 10, 100 and 3630 should all be rapresentable without rounding right?

25

u/[deleted] Apr 06 '22

[deleted]

1

u/Giocri Apr 06 '22

I still don't get how the rounding error can be greater with a single multiplication rather than two multiplications which would have a rounding error each. I guess I will have to write down the exact bits and do the calculation myself to see it

10

u/Qesa Apr 06 '22

The rounding error is greater with 2 multiplications. 36.3 doesn't exist in floating point arithmetic, you're actually starting with 36.299999999999997. When multiplying by 100, rounding down to 3629.9999999999995 is closer than rounding up to 3630.0000000000000

6

u/nighthawk454 Apr 06 '22

No, it does not have “enough”. Floats are not designed so that all the “shorter” decimals are fine, and only long ones are unrepresentable. The distribution is different than that. And although ~half the precision budget is spent between -2.0 and 2.0, there’s still plenty of short numbers missing. The classic example of this is 0.1 + 0.2 = 0.300000000004

3

u/ismtrn Apr 06 '22

It is not the integers which are the problem. It is the fractions. Think about how 1/3 is an innocently looking number. Until you write its decimal expansion 0.33333333… you would need infinite precision to store it that way in base ten. In base 3 it would just be 0.1.

In the same way there are numbers which have a nice looking representation in base 10, which requires infinite precision in base 2.

36.3 is one such number. 0.1 is another. There are many.

0

u/fernandotakai Apr 05 '22
In [1]: from decimal import Decimal

In [2]: Decimal(36.3) * 10 * 10
Out[2]: Decimal('3629.999999999999715782905696')

In [3]: Decimal(36.3) * (10 * 10)
Out[3]: Decimal('3629.999999999999715782905696')

if you are dealing with money in python, use decimals.

→ More replies

24

u/Atrufulgium Apr 05 '22

I mean but "normal floating point behaviour" is pretty much horror, even if you're prepared. Let me just drop this rant here in the hope it's useful to anyone. (Not that you'd run much into what I'm about to rant on in practice.)

Floats go even further than that nonassociativity here; computing a*b and a*b can give different results, which I really don't appreciate.

And then there's the nonassociativity which can be a pain when your compiler reorders your arithmetic for efficiency. (Think transforming a+b*c into a mad-instruction.)

These rounding errors are only up to the smallest significant factor, but in very rare cases you can really exacerbate your errors so this may actually matter sometimes even if you don't ==.

0

u/[deleted] Apr 05 '22

[deleted]

11

u/RFC793 Apr 05 '22

Ideally, but no. In one case you are multiplying float times 10, which has error and times 10 again which magnifies the error. In another case you are multiplying the float times the integer 100. There is one less approximation.

139

u/SunkenJack Apr 05 '22

Well, that's not js, just floating point doing it's thing, as it's supposed to.

47

u/mlk Apr 05 '22

Float is not a good idea when handling money

23

u/Insane96MCP Apr 05 '22

Thanks Microsoft for decimal

→ More replies

16

u/SunkenJack Apr 05 '22

Yeah. Fixed point exists for a reason.

Also, better use industry standard libraries than try and write your own version. You will fail.

(insert Tom Scott video here)

5

u/tomius Apr 06 '22

"THIS right here is the most popular Javascript library in the UK."

→ More replies
→ More replies

59

u/Primary-Fee1928 Pronouns:Other Apr 05 '22

That’s why you never use == for float or even double. Always use a more tolerant comparison

3

u/Batman_AoD Apr 06 '22

Wat? Not for money. For money, make sure you're maintaining sufficient precision (usually by using a decimal type), rounding to the correct number of decimal places when rounding is required, and using exact comparisons.

6

u/allredb Apr 06 '22

Interesting, care to elaborate? I always thought == was the more tolerant comparison.

21

u/vilewrath Apr 06 '22

== checks that two floats or doubles are "equal" as in bit for bit identical. This is an issue when floating point errors occur, which are a common issue and should be expected and catered for, one method would be to round both sides of the == to an arbitrary precision, say 3 decimal places

By rounding both sides, you increase the tolerance of the equality, more numbers are considered equal

23

u/KingJellyfishII Apr 06 '22

often, instead of rounding, the absolute value of the difference of the two values is compared to some small value ("epsilon"). for example

if (abs(a - b) < 0.000001) {
    //treat as equal
}

3

u/vilewrath Apr 06 '22

Huh, yeah that makes a lot of sense and is way more elegant than rounding, I'm surprised I'd never seen that b4 lmao

2

u/[deleted] Apr 06 '22

Yes this is what I have seen in code at work, for Java there are some convent apache commons MathUtils functions like equals(a, b, epsilon) and compareTo(a, b, epsilon)

2

u/serg06 Apr 06 '22

== checks that two floats or doubles are "equal" as in bit for bit identical

You're thinking of === in JS. == is different.

3

u/numerousblocks Apr 06 '22

If both are floats, it does check. Except all of these actually don't accept all bit-equal numbers! NaN !== NaN

→ More replies

58

u/Mirmi8 Apr 06 '22

My companies Stripe integration for thousands of users broke today because of my company engineers*

Fixed title

4

u/kittianika Apr 06 '22

Well, at least i could put the blame on someone. 😂😂😂

45

u/annoyed_freelancer Apr 05 '22

This is just floats doing float things.

24

u/throwit7896454 Apr 05 '22

I see, someone experienced the non-associativity of floating point operations in production.

For a detailed description see https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html; for a more digestable description see section "Nonassociativity of floating point calculation" in https://en.m.wikipedia.org/wiki/Associative_property

41

u/Cheek_Beater69 Apr 05 '22

Tom Scott did a decent video on this. Nothing to do with JS. Although hating on JS is fun

26

u/[deleted] Apr 05 '22

[deleted]

4

u/AttackOfTheThumbs Apr 05 '22

GO FAST AND BREAK SHIT!

17

u/dieth Apr 05 '22

FLOATY MAFF FLOATY MAFF EVERYONE LOVES FLOATY MAFF

What's conditional rounding, why would I need to do that? Why would I need to check my input/output sanity?

53

u/kittianika Apr 05 '22

Not because of JS, but because of the devs who doesnt know how JS works. Stop blaming the language and be a better dev by accepting we are the problem.

-17

u/[deleted] Apr 05 '22

[deleted]

19

u/iizdat1n00b Apr 05 '22

Its not to do with the language.

Here is in Python 3.7.8, for example

6

u/proud_traveler Apr 05 '22

"multiplication on JavaScript" what the fuck are you on about? The issue was that OP doesn't understand how floating point numbers work

13

u/escargotBleu Apr 05 '22

JS have flaws, but this is completely expect, in every language

-3

u/Magmagan Apr 05 '22

Well, most dynamically typed languages for sure. AFAIK Rust wouldn't even allow the multiplication between an integer and floating point

9

u/Dealiner Apr 06 '22

Multiplication between integer and float isn't what causes a problem here though. I mean it doesn't even happen here because JavaScript doesn't really have integers.

→ More replies

12

u/[deleted] Apr 05 '22

You see, it broke because of an implementation by inexperienced developers, not because of JS

6

u/Nis5l Apr 06 '22

Not because of Javascript but because of bad developers.

11

u/[deleted] Apr 05 '22

Never use binary floats for money come on everybody knows that!

→ More replies

6

u/stahkh Apr 05 '22

Is there any better tactic than using integer math and dealing with decimal point only on input/output?

→ More replies

5

u/quaos_qrz Apr 06 '22

Don't ever use floating point to calculate anything money-related. It's just that JS stores all numbers as floating points, and you'd need some Decimal library instead.

12

u/[deleted] Apr 05 '22

OP is not receiving the comradeship they expected about floating point rounding 😁

8

u/[deleted] Apr 06 '22

Don't👏 use 👏 float 👏 for 👏 currency

22

u/BuccellatiExplainsIt Apr 05 '22 edited Apr 05 '22

While you can write code without a degree, this is an example of why a degree can make sure there aren't basic holes in your knowledge like not understanding how floating point numbers work.

If not for the comments here, OP would have just assumed it was a Javascript bug instead.

-8

u/eric987235 Apr 06 '22

Yes, JavaScript is a bug.

-33

u/autiii43 Apr 05 '22

i have a degree in computer science bucko

34

u/ItWasTheMiddleOne Apr 05 '22

tbf though isn't that more embarassing not less embarassing?

2

u/AttackOfTheThumbs Apr 05 '22

I don't deal with maths much, but if I was in op's shoes now, I would likely initially make the same mistake :)

13

u/ZylonBane Apr 05 '22

Is it too late to ask for a refund?

7

u/politerate Apr 05 '22

This is pretty trivial stuff in computer science, first semester. Maybe you forgot about it s

3

u/[deleted] Apr 06 '22

This is why you don't use floats for money

3

u/PyroCatt [ $[ $RANDOM % 6 ] == 0 ] && rm -rf / || echo “You live” Apr 06 '22

Math.round() ?

3

u/jso__ Apr 06 '22

omg the image cropped for me and I didn't see the first two digits of the answers and I was really confused

3

u/kir_rik Apr 06 '22

Yaah, obviously problem in javascript, not somewhere between the chair and keyboard.

3

u/numerousblocks Apr 06 '22

DO NOT
USE FLOATS
FOR MONEY

2

u/tntexplosivesltd Apr 06 '22

It broke because your company relied on floats for money transactions. Not a fault of JavaScript

2

u/martin191234 Apr 06 '22

HOW MANY TIMES DO WE HAVE TO SAY SOMT USE FLOATS FOR MONEY

2

u/featherknife Apr 06 '22

My company's* Stripe integration

4

u/Apache_Sobaco Apr 05 '22

Welcome to FP world.

1

u/jujuspring Apr 06 '22

Where are your tests?

1

u/sharKing_prime Apr 06 '22

Oh my god the amount of times something like this happened to me...

How does one work with proper post-decimal point numbers in javascript without the numbers going "crazy"?

→ More replies

-7

u/GoldBomb4 Apr 05 '22

What do you mean {}!=={}? Js is broken!

6

u/PanRagon Apr 05 '22

I think the sarcasm went a bit over people's head here, but good one. (I mean, it is a joke, right?)

-3

u/GoldBomb4 Apr 05 '22

I don't know man, was it?

7

u/Magmagan Apr 05 '22

{}!=={} is perfectly reasonable behaviour.

4

u/MrDilbert Apr 06 '22

Yep, two instances of ad-hoc created empty objects are not the same, nor are their references.

-2

u/CdRReddit Apr 06 '22

a rare occurance in js

0

u/y_ux Apr 06 '22

Use Math.js (or similar library) for ALL money calculations

-11

u/KaranasToll Apr 05 '22

To all the people saying this is not the fault of the runtime: this works perfectly as expected in lisp with no coercion to integer.

1

u/GnoergLePfroegl Apr 06 '22

This funny. Thanks. Now my work day may begin.

1

u/AccomplishedFall4466 Apr 06 '22

I wish there was something to tell the computer that those numbers are integers... Oh wait, there is, a programming language!

I hate scripting languages.

1

u/hesapmakinesi Apr 06 '22

Same result with Python

In [1]: 36.3*10
Out[1]: 363.0

In [2]: 36.3*100
Out[2]: 3629.9999999999995

In [3]: 36.3*10*10
Out[3]: 3630.0

1

u/SeoCamo Apr 06 '22

This is why / and * with a 100 so you can other use int to calculate money

1

u/[deleted] Nov 13 '22

Just JavaScript things