After a lot of research I am actually using the following function to format numbers to the locale setted by the user:
function number_format_i18n($number, $decimals=0) {
$locale = localeconv();
return number_format($number,$decimals, $locale['decimal_point'], $locale['thousands_sep']);
}
I'll use this to format numbers from DB.
But if I have a form where a user can enter a number, I need a function to format it back so I can save into the DB. Here I found this solution:
function number_format_en($number) {
// $_SESSION['lang']['locale'] has the locale like de_DE, ar_AE, tr_TR, ...
$fmt = numfmt_create($_SESSION['lang']['locale'], NumberFormatter::DECIMAL);
return numfmt_parse($fmt, $number);
}
This works with the most locales I tested but not with everyone! As an example: if I use arabic (ar_AE):
$randomNumber = 3171003633.95;
$number2locale = number_format_i18n($randomNumber, 2);
// works as expected: 3,171,003,633.95
// now format it back:
$locale2number = number_format_en($number2locale);
// here I get this: 3.171
How can I format a locale entered number in a secure way "back" to en-format? Or is there a way to detect any kind of numberformat and format it to en-format so I can save it in the DB?
So, don't know really if this is a good solution or not - but it seems to be working fine:
function number_format_en($number) {
// first remove everything execpt -,.
$cleanNumber = preg_replace('/[^\\d-,.]+/', '', $number);
$last_dot = strrpos($cleanNumber, '.');
$last_comma = strrpos($cleanNumber, ',');
if($last_dot !== false || $last_comma !== false) {
if($last_dot > $last_comma) { // decimal seperator = dot
$decimal_point = '.';
if(substr_count($cleanNumber, '.') > 1) {
// could be totaly wrong 1,234.567.890
// or there are no decimals 1.234.567.890
// removing all dots and commas and returning the value
return preg_replace('/[^\\d-]+/', '', $cleanNumber);
}
} else { // decimal seperator = comma
$decimal_point = ',';
if(substr_count($cleanNumber, ',') > 1) {
// could be totaly wrong 1.234,567,890
// or there are no decimals 1,234,567,890
// removing all dots and commas and returning the value
return preg_replace('/[^\\d-]+/', '', $cleanNumber);
}
}
} else { // no decimals
$decimal_point = false;
$decimals = 0;
}
if($decimal_point !== false) {
// if decimals are delivered, get the count of them
$length = strlen($cleanNumber);
$position = strpos($cleanNumber, $decimal_point);
$decimals = $length - $position - 1;
if($decimal_point == '.') {
// remove all commas if seperator = .
$cleanNumber = str_replace(',', '', $cleanNumber);
} elseif($decimal_point == ',') {
// remove all dots if seperator = ,
$cleanNumber = str_replace('.', '', $cleanNumber);
// now switch comma with dot
$cleanNumber = str_replace(',', '.', $cleanNumber);
}
}
return $cleanNumber;
}
I'll check the last dot or comma and depending on that I format the number, function returns the same count of decimals as the original number. there is a "known" bug: if someone enters a full number like 1.000.000 or 1,000,000 it returns 1000000.
As an example:
number_format_en('1.234.567,89') // 1234567.89
number_format_en('-1,234,567.89') // -1234567.89
number_format_en('1.234.567.89') // 123456789 (only dots)
number_format_en('1,234,567,89') // 123456789 (only commas)
Would be nice if someone can post a comment if this is a good way or not or an answer with a better solution :)
Related
I have the following regex query that I built the idea is to take user input (phone numbers) and then make sure all the numbers are the exact same format when I store it in the database.
The problem I have is that this regex of mine does not cater for all the scenarios, I will explain in a bit.
This is my current code:
//format phone numbers
function Number_SA($number)
{
//strip out everything but numbers
$number = preg_replace("/[^0-9]/", "", $number);
//Strip out leading zeros:
$number = ltrim($number, '0');
//The default country code
$default_country_code = '+27';
//Check if the number doesn't already start with the correct dialling code:
if ( !preg_match('/^[+]'.$default_country_code.'/', $number) ) {
$number = $default_country_code.$number;
}
//return the converted number:
return $number;
}
The behavior I want is the following;
If a user enters any of the following formats the number should end up as +27
797734809 #missing a leading 0
0797734809
27797734809
+27797734809
Should all be converted to:
+27797734809
PS: I first clean the number input with this:
function clean_input($data)
{
$data = trim($data);
$data = stripslashes($data);
$data = htmlspecialchars($data);
return $data;
}
And then I use the Number_SA($number) function on the number.
In other words:
$number = clean_input($_POST["cell_number"]);
$msisdn = Number_SA($number);
PPS: Number_SA, the SA stands for South Africa since I live there and I want this to work for our country code which is +27
Solution, I ended up with #Kamil Kiełczewski answer by building it into my program as follows:
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (empty($_POST["cell_numbers"])) {
$err = "Please add numbers!";
} else {
$numbers = test_input($_POST["cell_numbers"]);
$numbers = explode("\n", str_replace("\r", "", $numbers));
foreach ($numbers as $number) {
$msisdn = preg_replace('/^(?:\+27|27|0)?/','+27', $number);
//insert into the database
}
}
}
Here is your regexp:
^(?:\+27|27|0)?(\d*)
and if it match something use this: +27$1
Here is working example (improved): https://regex101.com/r/VaUAKN/7
EXPLANATION:
First we wanna split prefix (if exists) with number: ^(?:\+27|27|0)? the ?: inside group makes that prefix group will be ignored in results. The ^ means begining of line. The last ? allow prefix to not exists. And at the end (\d*) catch numbers after prefix
PHP working example (Wiktor Stribiżew noted that in regexp we can can omit (\d*) and $1 and change \+27|27 to \+?27 ):
$numOld = "2712345678";
$num = preg_replace('/^(?:\+?27|0)?/','+27', $numOld); // > $num = "+2712345678"
You can use a switch to check which format the number is in, and alter it accordingly:
<?php
$x = [
'797734809',
'0797734809',
'27797734809',
'0027797734809',
'+27797734809',
];
function formatPhoneNo($no) {
switch (true) {
case (preg_match('#^7\d{8}$#', $no)):
$no = '+27' . $no;
break;
case (preg_match('#^07\d{8}$#', $no)):
$no = '+27' . substr($no, 1);
break;
case (preg_match('#^277\d{8}$#', $no)):
$no = '+' . $no;
break;
case (preg_match('#^00277\d{8}$#', $no)):
$no = '+' . substr($no, 2);
break;
case (preg_match('#^\+277\d{8}$#', $no)):
break;
default:
throw new InvalidArgumentException('Invalid format supplied');
break;
}
return $no;
}
foreach ($x as $y) {
echo formatPhoneNo($y) . "\n";
}
I added an extra format in your array, some people put 00 followed by the country code with no +. This outputs:
+27797734809
+27797734809
+27797734809
+27797734809
+27797734809
Which you can see here https://3v4l.org/ZVrNm
Data arrives as a string '99 10/32' or '99 5/32' or '100 5/32' or '100 25/32', etc
I need it in decimal form so I have done this, but results are not always correct:
...
$priceRaw = '99 5/32'; // ******also could be $priceRaw = 100 15/32, etc
$priceFrac = (str_replace("/32","",substr($priceRaw, -5))/100);
$priceFirst = (substr($priceRaw, 0, 3)*1);
$value = $priceFirst+$priceFrac;
// original code that failed with one digit, e.g. 5/32
// $value=str_replace("/32.","",str_replace(" ",".0",$e->plaintext));
...
Split the string by space to get parts
list($priceFirst, $priceFrac) = explode(' ', $priceRaw);
$priceFrac = (str_replace("/32","",$priceFrac)/100);
echo $value = $priceFirst+$priceFrac;
I'll get hammered for eval, but works for all fractions.
Split on space and use the numbers in a calculation:
$priceRaw = '99 5/32';
list($num, $frac) = explode(' ', $priceRaw);
eval("\$result = $num + $frac;");
echo $result; // 99.15625
Or replace the space with + and calculate:
$calc = str_replace(' ', '+', $priceRaw);
eval("\$result = $calc;");
echo $result; // 99.15625
Then just round or number_format or whatever you need. I may be missing something important as your math is funny.
Or we can go the regular expression route:
function convFrac($in) {
if( preg_match('#^(\d+)\s+(\d+)/(\d+)$#', $in, $match) ) {
$tmp = array_map('intval', array_slice($match, 1));
return $tmp[0] + $tmp[1] / $tmp[2];
}
throw new \Exception("Badly formatted fraction.");
}
var_dump( convFrac('99 5/32') ); // float(99.15625)
Mentioned below is a dummy Email ID say,
abcdefghij#gmail.com
How to mask this email ID partially using PHP?
Output i need as
a*c*e*g*i*#gmail.com
I have tried the below code, But it not works for below requirement
$prop=3;
$domain = substr(strrchr($Member_Email, "#"), 1);
$mailname=str_replace($domain,'',$Member_Email);
$name_l=strlen($mailname);
$domain_l=strlen($domain);
for($i=0;$i<=$name_l/$prop-1;$i++)
{
$start.='*';
}
for($i=0;$i<=$domain_l/$prop-1;$i++)
{
$end.='*';
}
$MaskMail = substr_replace($mailname, $start,2, $name_l/$prop).substr_replace($domain, $end, 2, $domain_l/$prop);
Give a try like this.
$delimeter = '#';
$mail_id = 'abcdefghij#gmail.com';
$domain = substr(strrchr($mail_id, $delimeter), 1);
$user_id = substr($mail_id,0,strpos($mail_id, $delimeter));
$string_array = str_split($user_id);
$partial_id = NULL;
foreach($string_array as $key => $val){
if($key % 2 == 0){
$partial_id .=$val;
}else{
$partial_id .='*' ;
}
}
echo $partial_id.$delimeter.$domain;
Here's a no loop approach to replace every second character of an email username with a mask.
Custom PHP function using native functions split, preg_replace with regex /(.)./, and implode:
echo email_mask('abcdefghi#gmail.com');
// a*c*e*g*i*k*#gmail.com
function email_mask($email) {
list($email_username, $email_domain) = split('#', $email);
$masked_email_username = preg_replace('/(.)./', "$1*", $email_username);
return implode('#', array($masked_email_username, $email_domain));
}
Regex Explanation:
The regular expression starts at the beginning of the string, matches 2 characters and captures the first of those two, replaces the match with the first character followed by an asterisk *. preg_replace repeats this throughout the remaining string until it can no longer match a pair of characters.
$mail='abcdefghij#gmail.com';
$mail_first=explode('#',$mail);
$arr=str_split($mail_first[0]);
$mask=array();
for($i=0;$i<count($arr);$i++) {
if($i%2!=0) {
$arr[$i]='*';
}
$mask[]=$arr[$i];
}
$mask=join($mask).'#'.$mail_first[1];
echo $mask;
Result is :
a*c*e*g*i*#gmail.com
Does it need to have that many asterisks?
It's so hard to read that way.
I will suggest you keep things simple.
Maybe something like this is enough
https://github.com/fedmich/PHP_Codes/blob/master/mask_email.php
Masks an email to show first 3 characters and then the last character before the # sign
ABCDEFZ#gmail.com becomes
A*****Z#gmail.com
Here is the full code that is also in that Github link
function mask_email( $email ) {
/*
Author: Fed
Simple way of masking emails
*/
$char_shown = 3;
$mail_parts = explode("#", $email);
$username = $mail_parts[0];
$len = strlen( $username );
if( $len <= $char_shown ){
return implode("#", $mail_parts );
}
//Logic: show asterisk in middle, but also show the last character before #
$mail_parts[0] = substr( $username, 0 , $char_shown )
. str_repeat("*", $len - $char_shown - 1 )
. substr( $username, $len - $char_shown + 2 , 1 )
;
return implode("#", $mail_parts );
}
I have a form where the users write different numbers and can be decimal, but the format is different from the PHP and MySQL default format. This is the format I work with:
1,1 (one dot one) - 1.000,90 (one thousand and 90) - 1.000.000,90 (one million and 90), etc
Right now I have this function:
function formato_num($num){
$num = str_replace('.', '', $num);
$num = str_replace(',', '.', $num);
return $num;
}
It works fine in those cases but, if the user for some reason writes the decimal with a dot (.), it doesn't work.
I have this other function, but it doesn't work completely fine:
if(strpos($_POST['num'],'.') !== false){
$decimal = $_POST['num'];
$split = explode('.', $decimal);
$l = strlen($split[1]);
if($l < 3){
echo str_replace('.', ',', $_POST['num']);
}else{
$num = str_replace('.', '', $_POST['num']);
$num = str_replace(',', '.', $_POST['num']);
echo $num;
}
}
Any help? Thanks.
EXAMPLES:
If the user writes:
1.000 (one thousand) I save it on the DDBB as 1000.
1,2 (decimal) I save it as 1.1.
The problem is if the user use a dot for the thousand separator and decimal.
You can "easily" transform strings containing both: a comma and a dot, but especially if you want to support misplaced thousand-separators or allow the user to enter three decimal places ambiguities arise.
This is a best-effort approach which you could use. I while not be responsible for any misinterpreted numbers!
function stringToNumber($str) {
$last_comma = strrpos($str, ',');
$last_dot = strrpos($str, '.');
if($last_comma === false && $last_dot === false) {
return $str;
} elseif($last_comma !== false && $last_dot !== false) {
if($last_comma < $last_dot) {
// dot is further to the right
return str_replace(',', '', $str);
} else {
// comma is further to the right
return str_replace(',', '.', str_replace('.', '', $str));
}
} elseif ($last_dot !== false) {
// No commas. For thousand-separator the following must hold
if(preg_match('/^[0-9]{1,3}(\\.[0-9]{3})+$/', $str)) {
// every dot is followed by three digits... lets assume the user wanted them as thousand-separators
// For a single dot this assumption may be invalid, but we expect the user to use . as thousand-separator...
return str_replace('.', '', $str);
} else {
// We could check here how many dots are used.
return $str;
}
} else {
// No dots. For thousand-separator the following must hold
if(preg_match('/^[0-9]{1,3}(,[0-9]{3})+,[0-9]{3}$/', $str)) {
// every comma is followed by three digits and there are at least two of them
return str_replace(',', '', $str);
} else {
// So this is it. Single comma. We could check if the comma is followed by three digits, but it still could be legitimate and while we want to support unexpected input we do not want to interfere with valid input like "1,234" meant as 1.234
return str_replace(',', '.', $str);
}
}
}
Examples:
function formated_test($str) {
$num = stringToNumber($str);
printf("% 14s => % 14s\n", $str, $num);
}
formated_test("42");
formated_test("42.1");
formated_test("42,1");
formated_test("42.123");
formated_test("42,123");
formated_test("42.123,42");
formated_test("42,123.42");
formated_test("42.123.456");
formated_test("42,123,456");
formated_test("42.123.456,12");
formated_test("42,123,456.12");
And output:
42 => 42
42.1 => 42.1
42,1 => 42.1
42.123 => 42123
42,123 => 42.123
42.123,42 => 42123.42
42,123.42 => 42123.42
42.123.456 => 42123456
42,123,456 => 42123456
42.123.456,12 => 42123456.12
42,123,456.12 => 42123456.12
I'd like to validate a numeric field (price) in my form.
I try in this way to validate format like 10.00 and it's ok.
$pattern = '/^\d+(:?[.]\d{2})$/';
if (preg_match($pattern, $_POST['price']) == '0') {
echo "ERROR";
exit;
}
Now I'd like to validate, at the same time, the field format like 10.00 and 10.
How could I do this?
Your new pattern:
$pattern = '/^\d+(\.\d{2})?$/';
will validate:
10
10.00
If you want to invalidate zero-leading numerics such as 05.00, the following pattern will help:
$pattern = '/^(0|[1-9]\d*)(\.\d{2})?$/';
If you're only checking if it's a number, is_numeric() is much much better here. It's more readable and a bit quicker than regex.
I quickly spun up a function for checking if a string or int is a valid price and if not, converting it. The final output is always going to be a string with two digits after the decimal, like 1000.00.
It removes minus/negative signs, commas, spaces, and dollar signs. The only except is when if the string contains any characters like letters that would set is_numeric() to false.
Here is the function:
function convertToValidPrice($price) {
$price = str_replace(['-', ',', '$', ' '], '', $price);
if(!is_numeric($price)) {
$price = null;
} else {
if(strpos($price, '.') !== false) {
$dollarExplode = explode('.', $price);
$dollar = $dollarExplode[0];
$cents = $dollarExplode[1];
if(strlen($cents) === 0) {
$cents = '00';
} elseif(strlen($cents) === 1) {
$cents = $cents.'0';
} elseif(strlen($cents) > 2) {
$cents = substr($cents, 0, 2);
}
$price = $dollar.'.'.$cents;
} else {
$cents = '00';
$price = $price.'.'.$cents;
}
}
return $price;
}
And here is to test it:
var_dump(convertToValidPrice('100')); // 100.00
var_dump(convertToValidPrice('-100')); // 100.00
var_dump(convertToValidPrice(100)); // 100.00
var_dump(convertToValidPrice('$100')); // 100.00
var_dump(convertToValidPrice('100.98')); // 100.98
var_dump(convertToValidPrice('100.9')); // 100.90
var_dump(convertToValidPrice('100.')); // 100.00
var_dump(convertToValidPrice('1,00.98')); // 100.98
var_dump(convertToValidPrice('1,000.98')); // 1000.98
var_dump(convertToValidPrice('100.98829382')); // 100.98
var_dump(convertToValidPrice('abc')); // null