0

This is an extension of this SO question

I made a function to see if i can correctly format any number. The answers below work on tools like https://regex101.com and https://regexr.com/, but not within my function(tried in node and browser): const

const format = (num, regex) => String(num).replace(regex, '$1')

Basically given any whole number, it should not exceed 15 significant digits. Given any decimal, it should not exceed 2 decimal points.

so... Now

format(0.12345678901234567890, /^\d{1,13}(\.\d{1,2}|\d{0,2})$/)

returns 0.123456789012345678 instead of 0.123456789012345

but

format(0.123456789012345,/^-?(\d*\.?\d{0,2}).*/)

returns number formatted to 2 deimal points as expected.

Fatah
  • 2,184
  • 4
  • 18
  • 39
  • Why use the first regex if it is only meant to match some specific number format while you need to remove all after the `.` and 1 or 2 digits? Use `format(0.12345678901234567890, /(\.\d{1,2}).*/)` – Wiktor Stribiżew Apr 18 '18 at 08:17
  • You won't match 12345678901234.1 (14 digits before decimal point) with your regex. Generally, using regex seems like a big overkill here, and `replace()` seems like a very wrong tool to use. `number.toFixed(0).length` is the simplest way to get number of digits before decimal point. `number.toFixed(Math.min(2, Math.max(0, 15 - number.toFixed(0).length)))` gets you roughly what you want. Add `.replace(/\.?0*$/, '')` to get rid of the trailing zeroes if they are an issue. It returns more than 15 digits for numbers >= `10^15`, but that's perhaps what you actually need. – Frax Apr 18 '18 at 09:03
  • Hey @Frax thanks alot. Rouding off will cause problems especially when dealing with many suh calculation steps or dealing with cryptocurrency trading – Fatah Apr 19 '18 at 07:00
  • Well, you have rounding anyway, it's just that in my version it's less biased (in your, it's always toward 0). So I'm not sure what is your problem here. Also, [rounding is inherent problem of floating point representation](https://stackoverflow.com/questions/960072/rounding-errors), so the problem is going to be there no matter what. Morover, you should [_never_ use floating point representation for _any_ currency](https://stackoverflow.com/questions/3730019/why-not-use-double-or-float-to-represent-currency/). Use fixed point arithmetics, decimal math library, or better suited language. – Frax Apr 19 '18 at 07:58
  • And, btw, a warning: neither your nor my solution supports negative numbers. – Frax Apr 19 '18 at 07:59

2 Answers2

2

Let me try to explain what's going on.

For the given input 0.12345678901234567890 and the regex /^\d{1,13}(\.\d{1,2}|\d{0,2})$/, let's go step by step and see what's happening.

  1. ^\d{1,13} Does indeed match the start of the string 0
  2. (\. Now you've opened a new group, and it does match .
  3. \d{1,2} It does find the digits 1 and 2
  4. |\d{0,2} So this part is skipped
  5. ) So this is the end of your capture group.
  6. $ This indicates the end of the string, but it won't match, because you've still got 345678901234567890 remaining.

Javascript returns the whole string because the match failed in the end.

Let's try removing $ at the end, to become /^\d{1,13}(\.\d{1,2}|\d{0,2})/

You'd get back ".12345678901234567890". This generates a couple of questions.

Why did the preceding 0 get removed?

Because it was not part of your matching group, enclosed with ().

Why did we not get only two decimal places, i.e. .12?

Remember that you're doing a replace. Which means that by default, the original string will be kept in place, only the parts that match will get replaced. Since 345678901234567890 was not part of the match, it was left intact. The only part that matched was 0.12.

csb
  • 674
  • 5
  • 13
  • hey @csb why do you think these doesn't `regex.test('0.12345678901234567890')` returns `false` and `regex.exec('0.12345678901234567890')` returns `null`. do you know why? – Fatah Apr 19 '18 at 06:35
  • I am not using `String.replace()` in the above – Fatah Apr 19 '18 at 06:42
  • Thanks for the clarity @csb, I dont understand that last part though. If the only part thats matched is `0.12`, why is it not returning that(`0.12`) after replace? – Fatah Apr 19 '18 at 06:46
  • No problem. I had a hard time with regex in the past - I feel your pain! Overall the regex is ***not*** matching, because of the `$` indicating that it should match the end of the string input, but there's still a whole bunch of digits left in your string. If you remove `$`, the string ***does*** indeed get replaced. But only the part that matches - `0.12` - gets replaced. And it so happens that it gets replaced by what was matched, which is `.12`. – csb Apr 19 '18 at 07:08
0

Answer to title question: your function doesn't replace, because there's nothing to replace - the regex doesn't match anything in the string. csb's answer explains that in all details.

But that's perhaps not the answer you really need.

Now, it seems like you have an XY problem. You ask why your call to .replace() doesn't work, but .replace() is definitely not a function you should use. Role of .replace() is replacing parts of string, while you actually want to create a different string. Moreover, in the comments you suggest that your formatting is not only for presenting data to user, but you also intend to use it in some further computation. You also mention cryptocurriencies.

Let's cope with these problems one-by-one.

What to do instead of replace?

Well, just produce the string you need instead of replacing something in the string you don't like. There are some edge cases. Instead of writing all-in-one regex, just handle them one-by-one.

The following code is definitely not best possible, but it's main aim is to be simple and show exactly what is going on.

function format(n) {
    const max_significant_digits = 15;
    const max_precision = 2;
    let digits_before_decimal_point;
    if (n < 0) {
        // Don't count minus sign.
        digits_before_decimal_point = n.toFixed(0).length - 1;
    } else {
        digits_before_decimal_point = n.toFixed(0).length;
    }
    if (digits_before_decimal_point > max_significant_digits) {
        throw new Error('No good representation for this number');
    }
    const available_significant_digits_for_precision =
        Math.max(0, max_significant_digits - digits_before_decimal_point);
    const effective_max_precision =
        Math.min(max_precision, available_significant_digits_for_precision);
    const with_trailing_zeroes = n.toFixed(effective_max_precision);
    // I want to keep the string and change just matching part,
    // so here .replace() is a proper method to use.
    const withouth_trailing_zeroes = with_trailing_zeroes.replace(/\.?0*$/, '');
    return withouth_trailing_zeroes;
}

So, you got the number formatted the way you want. What now?

What can you use this string for?

Well, you can display it to the user. And that's mostly it. The value was rounded to (1) represent it in a different base and (2) fit in limited precision, so it's pretty much useless for any computation. And, BTW, why would you convert it to String in the first place, if what you want is a number?

Was the value you are trying to print ever useful in the first place?

Well, that's the most serious question here. Because, you know, floating point numbers are tricky. And they are absolutely abysmal for representing money. So, most likely the number you are trying to format is already a wrong number.

What to use instead?

Fixed-point arithmetic is the most obvious answer. Works most of the time. However, it's pretty tricky in JS, where number may slip into floating-point representation almost any time. So, it's better to use decimal arithmetic library. Optionally, switch to a language that has built-in bignums and decimals, like Python.

Community
  • 1
  • 1
Frax
  • 5,015
  • 2
  • 17
  • 19