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

[RFC] Add bcfloor, bcceil and bcround to BCMath #13096

Merged
merged 18 commits into from Apr 30, 2024

Conversation

SakiTakamachi
Copy link
Member

@SakiTakamachi SakiTakamachi force-pushed the rfc/bcmath-add-functions branch 2 times, most recently from e189f49 to 469b079 Compare January 9, 2024 08:22
@SakiTakamachi SakiTakamachi changed the title RFC: Add bcfloor, bcceil and bcround to BCMath [RFC] Add bcfloor, bcceil and bcround to BCMath Jan 9, 2024
@SakiTakamachi SakiTakamachi marked this pull request as ready for review January 9, 2024 08:57
@SakiTakamachi
Copy link
Member Author

ping @kocsismate

There is a change in the stub, but you aren't included in the reviewers 🤔

@kocsismate
Copy link
Member

There is a change in the stub, but you aren't included in the reviewers 🤔

Thanks for letting me know! I was wondering indeed for a long time, why I'm not included in many of the PRs changing stubs. After some investigation, I realized the problem and pushed d54e2f9 (details are in the description).

Regarding your PR, it looks fine to me as far as I'm qualified to tell, but I'm not a math expert... So I cannot approve it. :/ Hopefully, Gina or Niels can help review it. :)

bc_init_num(&result);

if (php_str2num(&num, ZSTR_VAL(numstr)) == FAILURE) {
zend_argument_value_error(1, "is not well-formed");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I miss tests for the error cases (this one and a few more below)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for checking!
I added value error case to tests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a case with a nul byte in the string. The current API doesn't support them as it basically ignores anything past the nul byte.

Copy link
Member

@nielsdos nielsdos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't look thoroughly at the tests or tested this myself, but I have some preliminary feedback.
It mostly looks correct.

ext/bcmath/bcmath.c Show resolved Hide resolved
ext/bcmath/bcmath.c Show resolved Hide resolved
ext/bcmath/libbcmath/src/floor_or_ceil.c Outdated Show resolved Hide resolved
ext/bcmath/libbcmath/src/round.c Outdated Show resolved Hide resolved
@SakiTakamachi
Copy link
Member Author

@nielsdos
I fixed them

Copy link
Member

@Girgias Girgias left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't have a proper look at the test results yet

ext/bcmath/tests/bcceil.phpt Outdated Show resolved Hide resolved
ext/bcmath/tests/bcfloor.phpt Outdated Show resolved Hide resolved
ext/bcmath/tests/bcceil.phpt Outdated Show resolved Hide resolved
bc_init_num(&result);

if (php_str2num(&num, ZSTR_VAL(numstr)) == FAILURE) {
zend_argument_value_error(1, "is not well-formed");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a case with a nul byte in the string. The current API doesn't support them as it basically ignores anything past the nul byte.

Western Washington University
Bellingham, WA 98226-9062

*************************************************************************/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This license header file looks invalid, not sure what should be used here. Be that the usual PHP License or another one.

@ramsey do you know by any chance what would be a reasonable licence to use here?

*
* 1. When rounding to an integer part, when trying to round to a digit
* larger than the num.
* 2. If try to round to the finest digit of num or finer than that,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by "finest" here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean "smallest digit". For example, if num is 1.23456, that digit means 6.
What is the appropriate way to express it in English?

ext/bcmath/libbcmath/src/round.c Outdated Show resolved Hide resolved
ext/bcmath/libbcmath/src/round.c Outdated Show resolved Hide resolved
/* Loop through the remaining digits. */
size_t count = num->n_len + num->n_scale - rounded_len - 1;
nptr++;
while ((count > 0) && (*nptr++ == 0)) count--;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CS

Suggested change
while ((count > 0) && (*nptr++ == 0)) count--;
while ((count > 0) && (*nptr++ == 0)) {
count--;
}

Also am I missing something or why are we only checking if the digit is 0 ? This does not seem to take into account the case where we need to decide if we need to go up or down on 5?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, for the following code:

bcround('0.250000', 1, PHP_ROUND_HALF_UP);

Here it is checking for 0000 after 0.25. If num is 0.250001, it is not an edge case, so it judge whether it is an edge case by checking whether all the remaining values ​​are 0.

@@ -19,6 +19,8 @@

#include "libbcmath/src/bcmath.h"
#include "zend_API.h"
#include "php.h"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this need to be included?

I tried to get rid of plenty of includes in bcmath.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to read and use round constants from php_math.h, but I need to resolve PHPAPI.
If you have any good ideas, it would be helpful if you could let me know.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhh right it's that issue... one "dumb" solution is to redefine the constants for BCMATH and expose them and have a PHPT test to check that the values are really equal to each other.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been thinking about this for a while now, how if I separate out just the standard round-mode constants into a file like "php_math_round.h" and include that?

ext/bcmath/libbcmath/src/round.c Outdated Show resolved Hide resolved
/* check fractional part. */
size_t count = num->n_scale;
char *nptr = num->n_value + num->n_len;
while ((count > 0) && (*nptr++ == 0)) count--;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this can become an additional utility function in bc_zero.c, but it's not too important.

@SakiTakamachi
Copy link
Member Author

Thank you all, I will check and correct them one by one!

@SakiTakamachi
Copy link
Member Author

@Girgias

Please add a case with a nul byte in the string. The current API doesn't support them as it basically ignores anything past the nul byte.

I think it would probably be better to add a null check to php_str2num, what do you think?

@SakiTakamachi
Copy link
Member Author

SakiTakamachi commented Mar 10, 2024

In response to some comments, and have split the test.

2d0a52f
304cde9

Copy link
Member

@Girgias Girgias left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have checked the tests for ceil and floor and they seem correct.

Haven't done rounding modes yet.

The licence is still leaving me puzzled.

ext/bcmath/tests/bcceil.phpt Outdated Show resolved Hide resolved
ext/bcmath/libbcmath/src/round.c Outdated Show resolved Hide resolved
@SakiTakamachi
Copy link
Member Author

I set the test expected value to left pad and the license to php license.

--EXPECT--
0 => 0
0.00 => 0
-0 => 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether the result should be -0 ? (Similarly for cases below and for bcceil)
It would be consistent with: var_dump(floor(-0.0));
Genuine question, I don't know the answer for sure.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BCMath does not currently support -0.

signch = num->n_sign != PLUS && !bc_is_zero_for_scale(num, MIN(num->n_scale, scale));

https://3v4l.org/HteIO

It seems worth supporting, but I'd like to keep it in a separate scope from this pull request.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I understand.

ext/bcmath/tests/bcround_early_return.phpt Outdated Show resolved Hide resolved
Copy link
Member

@nielsdos nielsdos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm. Maybe wait for Gina for a final review round, as she is the code owner.
Also don't forget to add an entry to NEWS and UPGRADING.

@SakiTakamachi
Copy link
Member Author

@nielsdos
Of course, thanks!

@SakiTakamachi
Copy link
Member Author

BTW, Regarding the second argument of bcround, during the discussion on object support, it was pointed out that precision may have a different meaning.
The argument names of standard's round() are also the same, what do you think is the best way to do this?

@Girgias
Copy link
Member

Girgias commented Apr 14, 2024

Brilliant, so PHP is just wrong: https://www.calculatorsoup.com/calculators/math/rounding-methods-calculator.php?method=round+half+down&x=-1.5&round=0&action=solve

Half up means to always do +0.5 on the boundary, and half down means to always do -0.5 on the boundary.
See: https://en.wikipedia.org/wiki/Rounding

So PHP has just been wrong for decades. But at least it is in good company in that Java, Python, Ruby, and probably others also use the wrong terminology.

I'm going to email internals explaining this situation, as I'm not sure how to "fix" this, especially in light of: https://wiki.php.net/rfc/new_rounding_modes_to_round_function

@SakiTakamachi
Copy link
Member Author

@Girgias
Thanks for your mail.

There also seem to be missing some tests for integer precisions being negative on the boundary: e.g. 50 with a precision of -3

Since this is an early return pattern, it is included in bcround_early_return.phpt.

@Girgias
Copy link
Member

Girgias commented Apr 14, 2024

@Girgias Thanks for your mail.

There also seem to be missing some tests for integer precisions being negative on the boundary: e.g. 50 with a precision of -3

Since this is an early return pattern, it is included in bcround_early_return.phpt.

I might be blind, but I don't think I saw a case where all the different half rounding modes are tested on such a boundary condition.

@SakiTakamachi
Copy link
Member Author

@Girgias

I might be blind, but I don't think I saw a case where all the different half rounding modes are tested on such a boundary condition.

Indeed, I only tested HALF_UP, thank you. Added all modes to bcround_early_return.phpt.

Copy link
Member

@Girgias Girgias left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You still haven't added the tests I want, I want something like the following:

$modes = [
    'PHP_ROUND_HALF_UP',
    'PHP_ROUND_HALF_DOWN',
    'PHP_ROUND_HALF_EVEN',
    'PHP_ROUND_HALF_ODD',
    'PHP_ROUND_FLOOR',
    'PHP_ROUND_CEILING',
    'PHP_ROUND_AWAY_FROM_ZERO',
    'PHP_ROUND_TOWARD_ZERO',
];

foreach ($modes as $mode) {
    var_dump(round(50, -2, constant($mode)));
}

which, according to 3v4l.org (https://3v4l.org/8J3Mu/rfc#vgit.master) returns:

float(100)
float(0)
float(0)
float(100)
float(0)
float(100)
float(100)
float(0)

Added boundary value test case. Also removed test cases that should not be returned early from early return tests
@SakiTakamachi
Copy link
Member Author

SakiTakamachi commented Apr 15, 2024

@Girgias

You have good eyes. Indeed, that test case failed.

I added and modified the test cases, then modified the code. As you taught me before, I am splitting commits.

Thanks!

(edit)
Ah, after looking at it again, I think I can write it a little more efficiently.

The comparison conditions for determining early returns were incorrect and have been corrected.

Added processing when the rounding result is carried forward or becomes 0.
@SakiTakamachi
Copy link
Member Author

Fix completed

Copy link
Member

@Girgias Girgias left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We got there in the end 🎉

Other than a very minor comment as I don't see why this works, but the tests look correct now :)

}
break;

case PHP_ROUND_HALF_DOWN:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't the half down case be determined without a loop? If it does require a loop how come HALF UP doesn't need one?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, let's compare two cases: 5.0000 and 5.0001.

HALF_UP: Either way, it'll be a 10.
HALF_DOWN: 5.0001 will be 10, but 5.0000 will be 0.

Therefore, in the case of HALF_UP, there is no need to loop because it always carries up regardless of the loop result.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait, I was refactoring BCMath in another PR and found that trailing zeros are discarded when converting strings to bc_num structs.

So in all cases I don't need to loop, just check if there is one more digit.

@Girgias Girgias merged commit 5359392 into php:master Apr 30, 2024
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants