I'm having a problem with my code where NumberFormatter::parse() is truncating decimals when there is a trailing zero. Our app uses Intl to be locale aware, so I am doing a JSON API call to check to make sure a number entered in a form is actually a number. For example when the locale calls for a comma as a decimal separator return FALSE for numbers entered with a period.
function parse_decimals($number)
{
// ignore empty strings and return
if(empty($number))
{
return $number;
}
$config = get_instance()->config;
$fmt = new \NumberFormatter( $config->item('number_locale'), \NumberFormatter::DECIMAL);
$fmt->format(1234567890.12300);
if (empty($config->item('thousands_separator')))
{
$fmt->setAttribute(\NumberFormatter::GROUPING_SEPARATOR_SYMBOL, '');
}
try
{
if(strcmp(strval($fmt->parse($number)),strval($number)) != 0)
{
$number = FALSE;
}
return $fmt->parse($number);
}
catch (Exception $e)
{
return FALSE;
}
}
The locale doesn't matter (because it's happeneing with all locales, so for the sake of argument Intl is used to set the locale to en_US.
Entered 50.00
Expected 50.00
Actual 50
Return Result FALSE
So you can see that parse is truncating 50.00 to 50. I have tried playing around with NumberFormatter::MAX_FRACTION_DIGITS, NumberFormatter::MIN_FRACTION_DIGITS and NumberFormatter::FRACTION_DIGITS without any effect. A side note is that if there is a better way to compare the expected number's format with the entered number's format than this, please feel free to suggest something better than this:
if(strcmp(strval($fmt->parse($number)),strval($number)) != 0)
{
$number = FALSE;
}
Related
I have two variables in a PHP program for billing statements, $charges and $payments.
$charges is the total amount due before any payments. $payments is the total amount received.
I calculate the balance due like so:
$balance_due = $charges-$payments;
Simple, except I am getting the following result:
$balance_due has -9.0949470177293E-13 for a value (expecting 0).
Both $charges and $payments have a value of 5511.53.
When I var_dump($charges) and var_dump($payments) they both show: float(5511.53)
This code (and === ):
if($charges == $payments){
error_log('they are the same');
}else{
error_log('they are not the same');
}
both result in false.
If I hard code: $charges = $payments = 5511.53; and run it then $balance_due = 0 as expected.
I am confused. What am I missing?
EDIT NOTES
I was able to use a user contributed function by Nitrogen found on the BC Math Functions page that was suggested I look at in order to come up with the following solution:
if(Comp($charges, $payments)===0){
$balance_due = 0;
}else{
$balance_due = ( $charges - $payments );
}
function Comp($Num1,$Num2,$Scale=null) {
// check if they're valid positive numbers, extract the whole numbers and decimals
if(!preg_match("/^\+?(\d+)(\.\d+)?$/",$Num1,$Tmp1)||
!preg_match("/^\+?(\d+)(\.\d+)?$/",$Num2,$Tmp2)) return('0');
// remove leading zeroes from whole numbers
$Num1=ltrim($Tmp1[1],'0');
$Num2=ltrim($Tmp2[1],'0');
// first, we can just check the lengths of the numbers, this can help save processing time
// if $Num1 is longer than $Num2, return 1.. vice versa with the next step.
if(strlen($Num1)>strlen($Num2)) return(1);
else {
if(strlen($Num1)<strlen($Num2)) return(-1);
// if the two numbers are of equal length, we check digit-by-digit
else {
// remove ending zeroes from decimals and remove point
$Dec1=isset($Tmp1[2])?rtrim(substr($Tmp1[2],1),'0'):'';
$Dec2=isset($Tmp2[2])?rtrim(substr($Tmp2[2],1),'0'):'';
// if the user defined $Scale, then make sure we use that only
if($Scale!=null) {
$Dec1=substr($Dec1,0,$Scale);
$Dec2=substr($Dec2,0,$Scale);
}
// calculate the longest length of decimals
$DLen=max(strlen($Dec1),strlen($Dec2));
// append the padded decimals onto the end of the whole numbers
$Num1.=str_pad($Dec1,$DLen,'0');
$Num2.=str_pad($Dec2,$DLen,'0');
// check digit-by-digit, if they have a difference, return 1 or -1 (greater/lower than)
for($i=0;$i<strlen($Num1);$i++) {
if((int)$Num1{$i}>(int)$Num2{$i}) return(1);
else
if((int)$Num1{$i}<(int)$Num2{$i}) return(-1);
}
// if the two numbers have no difference (they're the same).. return 0
return(0);
}
}
}
That solution worked for me. The answer provided by imtheman below also works and seems more efficient so I am going to use that one instead. Is there any reason not to use one or the other of these?
The way I solved this problem when I ran into it was using php's number_format(). From php documentation:
string number_format(float $number [, int $decimals = 0 ])
So what I would do is this:
$balance_due = number_format($charges-$payments, 2);
And that should solve your problem.
Note: number_format() will return a string, so to compare it you must use == (not ===) or cast it back into a (float) before comparison.
I need to check if a parameter (either string or int or float) is a "large" integer. By "large integer" I mean that it doesn't have decimal places and can exceed PHP_INT_MAX. It's used as msec timestamp, internally represented as float.
ctype_digit comes to mind but enforces string type. is_int as secondary check is limited to PHP_INT_MAX range and is_numeric will accept floats with decimal places which is what I don't want.
Is it safe to rely on something like this or is there a better method:
if (is_numeric($val) && $val == floor($val)) {
return (double) $val;
}
else ...
I recommend the binary calculator as it does not care about length and max bytes. It converts your "integer" to a binary string and does all calculations that way.
BC math lib is the only reliable way to do RSA key generation/encryption in PHP, and so it can easy handle your requirement:
$userNumber = '1233333333333333333333333333333333333333312412412412';
if (bccomp($userNumber, PHP_INT_MAX, 0) === 1) {
// $userNumber is greater than INT MAX
}
Third parameter is the number of floating digits.
So basically you want to check if a particular variable is integer-like?
function isInteger($var)
{
if (is_int($var)) {
// the most obvious test
return true;
} elseif (is_numeric($var)) {
// cast to string first
return ctype_digit((string)$var);
}
return false;
}
Note that using a floating point variable to keep large integers will lose precision and when big enough will turn into a fraction, e.g. 9.9999999999991E+36, which will obviously fail the above tests.
If the value exceeds INT_MAX on the given environment (32-bit or 64-bit), I would recommend using gmp instead and persist the numbers in a string format.
function isInteger($var)
{
if (is_int($var)) {
return true;
} elseif (is_numeric($var)) {
// will throw warning
if (!gmp_init($var)) {
return false;
} elseif (gmp_cmp($var, PHP_INT_MAX) >0) {
return true;
} else {
return floor($var) == $var;
}
}
return false;
}
I did
at the end of the function to check for numeric data.
return is_numeric($text)&&!(is_int(strpos($text,".",0)));
It will first check if it is numeric then check if there is no decimal in the string by checking if it found a position. If it did the returned position is an int so is_int() will catch it.
(strpos($text,".",0)==FALSE) would also work based on the strpos manual but sometimes the function seems to send nothing at all back like
echo (strpos($text,".",0));
could be nothing and the ==FALSE is needed.
So I know there have been multiple questions regarding Money and converting to and from cents.
Heck I have even asked another one, but I want to make a slightly different question so I hope there are no duplicates out there.
So I have created a function that takes a Dollar Value and sends it to CENTS.
But I think I have a slight problem with my code and hoping I can get it tweaked a little.
$money4 = "10.0001";
// Converted to cents, as you can see it's slightly off.
$money41 = "1001";
// So when "1001", get's put in the database, and then I return it back as a Money variable.
// We get, "$10.01"... but what I have now is a leak in my amounts... as it rounded up to the second point.
So to do what I have done, I have used to functions I made to do this.
// This essentially gets a DOLLAR figure, or the CENT's Figure if requested.
function stripMoney($value, $position = 0, $returnAs = "")
{
// Does it even have a decimal?
if(isset($value) && strstr($value, ".")) {
// Strip out everything but numbers, decimals and negative
$value = preg_replace("/([^0-9\.\-])/i","",$value);
$decimals = explode(".", $value);
// Return Dollars as default
return ($returnAs == "int" ? (int)$decimals[$position] : $decimals[$position]);
} elseif(isset($value)) {
// If no decimals, lets just return a solid number
$value = preg_replace("/([^0-9\.\-])/i","",$value);
return ($returnAs == "int" ? (int)$value : $value);
}
}
The next function I use is to generate the CENTS or return it back as dollars.
function convertCents($money, $cents = NULL, $toCents = TRUE)
{
if(isset($money)) {
if($toCents == TRUE) {
// Convert dollars to cents
$totalCents = $money * 100;
// If we have any cents, lets add them on as well
if(isset($cents)) {
$centsCount = strlen($cents);
// In case someone inputs, $1.1
// We add a zero to the end of the var to make it accurate
if($centsCount < 2) {
$cents = "{$cents}0";
}
// Add the cents together
$totalCents = $totalCents + $cents;
}
// Return total cents
return $totalCents;
} else {
// Convert cents to dollars
$totalDollars = $money / 100;
return $totalDollars;
}
}
}
And the final function that puts everything together. So we just use 1 function to merge the 2 functions together basically.
function convertMoney($value, $toCents = TRUE) {
if(isset($value) && strstr($value, ".")) {
return convertCents(stripMoney($value, 0), stripMoney($value, 1), $toCents);
} elseif(!empty($value)) {
return convertCents(stripMoney($value, 0), NULL, $toCents);
}
}
What I have done might be overkill, But I think it's fairly solid, other than this 1 detail, that I can see.
can anyone help me with these adjustments?
Do not use floating point arithmetic if you need exact answers. This applies to almost all languages, not just PHP. Read the big warning in the PHP manual.
Instead check out BC Math or the GMP extension. The latter only works with integer numbers so you are probably most interested in BC Math.
I think money_format is the function you were looking for...
<?php
$number = 1234.56;
// let's print the international format for the en_US locale
setlocale(LC_MONETARY, 'en_US');
echo money_format('%i', $number) . "\n";
// USD 1,234.56
// Italian national format with 2 decimals`
setlocale(LC_MONETARY, 'it_IT');
echo money_format('%.2n', $number) . "\n";
// Eu 1.234,56
?>
I'm working on a function to validate a US phone number submitted by a user, which can be submitted in any of the popular number formats people usually use. My code so far is as follows:
$number = '123-456-7890';
function validate_telephone_number($number) {
$formats = array(
'###-###-####',
'(###)###-###',
'(###) ###-###',
'##########'
);
$number = trim(preg_replace('[0-9]', '#', $number));
if (in_array($number, $formats)) {
return true;
} else {
return false;
}
}
First off, this code does not seem to be working, and returns false on all submitted numbers. I can't seem to find my error.
Secondly, I'm looking for an easy way to only allow phone numbers from an array of specific allowed area codes. Any ideas?
For your first question:
preg_replace('/[0-9]/', '#', $number)
or '/\d/'
For the second question this may help you:
$areaCode = substr(preg_replace('/[^\d]/', '', $number),0 , 3);
This will give you the first 3 digits in the number by discarding all other characters.
I'm not familiar with the US area codes format so I cannot help you more with this one.
Bonus:
if (in_array($number, $formats)) {
return true;
} else {
return false;
}
is equivalent to
return in_array($number, $formats);
As a matter of fact any statement of the form
if(<expression>){
return true;
}
else{
return false;
}
can be written as return (bool) <expr>;, but in this case in_array will always return a Boolean so (bool) is not needed.
Your code does not check for well formatted but invalid numbers - for example, no area code starts with 0 or 1 in the US, so this could be checked. Also, your formats do not allow for country code inclusion - +15551234567 would be rejected, for example.
If you don't care about the formatting and just want to validate if the digits in the input amount to a valid US phone number, you could use something like this:
$clean_number = preg_replace("/[^0-9]/", '', $number);
$valid = preg_match("/^(\+?1)?[2-9][0-9]{9}$/", $clean_number);
Of course, this will also accept "foo 5555555555 bar" as a valid number - if you want to disallow that, make the preg_replace more restrictive (e.g, remove only brackets, spaces and dashes).
If you prefer to do this without maintaining a lot of code, you an check out this API that validates a US number and provides several formats for the number https://www.mashape.com/parsify/format
Look here for a code project that has a function for validating phone numbers.
I'm using this function to check if binary is correct, I know it looks sloppy.. I'm not sure how to write the function that well.. but it doesn't seem to work!
If binary = 10001000 it says malformed, even though it's not.. what is wrong in my function?..
function checkbinary($bin) {
$binary = $bin;
if(!strlen($binary) % 8 == 0){
return 1;
}
if (strlen($binary) > 100) {
return 1;
}
if (!preg_match('#^[01]+$#', $binary)){ //Tried without !
return 1;
}
if (!is_numeric($binary)) {
return 1;
}
}
if (checkbinary("10001000") != 1) {
echo "Correct";
} else {
echo "Binary incorrect";
}
Why does this function always say 10001000 is incorrect?
if(!strlen($binary) % 8 == 0){ should be
if( strlen($binary) % 8 !== 0 ){
edit and btw: Since you're already using preg_match() you can simplify/shorten the function to
function checkbinary($binary) {
return 1===preg_match('#^(?:[01]{8}){0,12}$#', $binary);
}
This allows 0 - 12 groups of 8 0/1 characters which includes all the tests you currently have in your function:
strlen()%8 is covered by {8} in the inner group
strlen() > 100 is covered by {0,12} since any string longer than 8*12=96 characters would trigger either the first if or the >100 test
0/1 test is obvious
is_numeric is kinda superfluous
edit2: The name checkbinary might not be a perfect choice for the function. I wouldn't necessarily expect it to check for 8bit/byte alignment and strlen()<=100.
function checkbinary($bin) {
return preg_match('#^[01]+$#', $bin);
}
Some tests: http://www.ideone.com/3D9SQetX and http://www.ideone.com/1HCCtxVV.
I'm not entirely sure what you're attempting to achieve, but you might be able to get there via the following...
if($binaryString == base_convert($binaryString, 2, 2))...
In essence, this is comparing the results of a conversion from binary to binary - hence if the resultant output is identical, the input must be a valid binary string.