PHP 2 chars decrement (AB -> AA) [duplicate] - php

This question already has answers here:
Increment letters like number by certain value in php
(2 answers)
Closed 4 years ago.
I am implementing some export functions using PHPExcel.
Since PHP can increment alphabet automatically it had been working fine but I have an issue when trying to decrement it.
I can decrement a single character like this $decremented = chr(ord($someChar) - 1);, but it does not work on 2 characters (such as 'AA','BB', .. etc.)
Is there any way that I can decrement two characters? Like 'ZZ' -> 'ZX', 'AA'->'Z'
Any help or thoughts would be really appreciated!

Here's a decrement function that will work for you:
function decrement($str) {
$index = strlen($str)-1;
$ord = ord($str[$index]);
if ($ord > 65) {
// The final character is still greater than A, decrement
return substr($str, 0, $index) . chr($ord-1);
}
if ($index > 0) {
// Strip the final 2 characters and append a Z
return substr($str, 0, $index-1) . 'Z';
}
// Can't be decremented
return false;
}
https://3v4l.org/WaaKY

Somebody wrote a function for this here.
function decrementLetter($char) {
$len = strlen($char);
// last character is A or a
if(ord($char[$len - 1]) === 65 || ord($char[$len - 1]) === 97){
if($len === 1){ // one character left
return null;
}
else{ // 'ABA'--; => 'AAZ'; recursive call
$char = decrementLetter(substr($char, 0, -1)).'Z';
}
}
else{
$char[$len - 1] = chr(ord($char[$len - 1]) - 1);
}
return $char;
}

Related

Format number to N significant digits in PHP

I would like to format (round) float (double) numbers to lets say 2 significant digits for example like this:
1 => 1
11 => 11
111 => 110
119 => 120
0.11 => 0.11
0.00011 => 0.00011
0.000111 => 0.00011
So the arbitrary precision remains same
I expect there is some nice function for it already built in, but could not find any so far
I was pointed to How to round down to the nearest significant figure in php, which is close but doesn't work for N significant digits and I'm not sure what it does with 0.000XXX numbers
To get a number rounded to n significant figures you need to find the size of the number in powers of ten, and subtract that from n.
This works fine for simple rounding:
function sigFig($value, $digits)
{
if ($value == 0) {
$decimalPlaces = $digits - 1;
} elseif ($value < 0) {
$decimalPlaces = $digits - floor(log10($value * -1)) - 1;
} else {
$decimalPlaces = $digits - floor(log10($value)) - 1;
}
$answer = round($value, $decimalPlaces);
return $answer;
}
This will give the following:
0.0001234567 returns 0.0001235
123456.7 returns 123500
However a value such as 10 to four significant figures should strictly be represented as 10.00 to signify the precision to which the value is known.
If this is the desired output you can use the following:
function sigFig($value, $digits)
{
if ($value == 0) {
$decimalPlaces = $digits - 1;
} elseif ($value < 0) {
$decimalPlaces = $digits - floor(log10($value * -1)) - 1;
} else {
$decimalPlaces = $digits - floor(log10($value)) - 1;
}
$answer = ($decimalPlaces > 0) ?
number_format($value, $decimalPlaces) : round($value, $decimalPlaces);
return $answer;
}
Now 1 is displayed as 1.000
With little modification to possible duplicate, answer by Todd Chaffee:
public static function roundRate($rate, $digits)
{
$mod = pow(10, intval(round(log10($rate))));
$mod = $mod / pow(10, $digits);
$answer = ((int)($rate / $mod)) * $mod;
return $answer;
}
To make sigFig(0.9995, 3) output 1.00, use
if(floor(log10($value)) !== floor(log10(round($value, $decimalPlaces)))) {$decimalPlaces--;}
Said line of code should be placed before declaring $answer.
If input $value is negative, set a flag and remove the sign at the beginning of the function, like this:
if($value < 0){$flag = 1;}
$value = ltrim($value, "-");
Then right before returning $answer, detect if the flag is set and if so restore the negative sign, like this:
if(isset($flag)){$answer = "-".$answer;}
Finally, for result values with ambiguous number of significant digits (e.g., 1000, 12000,...), express the result in scientific notation to the desired number of significant digits using sprintf or printf.

Count number of leading characters of a specific character at the beginning of a string?

Given a string such as:
$a = '00023407283';
$b = 'f045602345';
Is there a built in function that can count the number of occurrences of a specific character starting at the beginning and continuing until it finds a different character that is not specified?
Given the above, and specifying zero (0) as the character, the expected result would be:
$a = '00023407283'; // 3 (the other zeros don't count)
$b = 'f0045602345'; // 0 (It does not start with zero)
This should do the trick:
function count_leading($haystack,$value) {
$i = 0;
$mislead = false;
while($i < strlen($haystack) && !$mislead) {
if($haystack[$i] == $value) {
$i += 1;
} else {
$mislead = true;
}
}
return $i;
}
//examples
echo count_leading('aaldfkjlk','a'); //returns 2
echo count_leading('dskjheelk','c'); //returns 0
I don't think there's any built-in functions that could do that (it's too specific) but you could write a method to do that
function repeatChar($string, $char) {
$pos = 0;
while($string{$pos} == $char) $pos++;
return $pos;
}
Yes, you want strspn, which counts the number of characters from the second argument at the beginning of the first argument:
echo strspn($a, '0'); // === 3
echo strspn($b, '0'); // === 0
See it live at 3v4l.org. Besides being a built-in (read "fast"), this also accepts any number of single characters to look at the beginning. However, note that the function is byte-oriented, so it will not work as expected for multi-byte characters.

How write all possible words in php? [duplicate]

This question already has answers here:
Closed 11 years ago.
Possible Duplicate:
Generate all combinations of arbitrary alphabet up to arbitrary length
I'm trying to make write all possible words of 10 letters(zzzzzzzzzz) in php. How can I do that? it will look like that : http://i.imgur.com/sgUnL.png
I tried some ways to it but they are only making 10 letters randomly not increasing from 1 letter. By the way execution time and how it's big is not problem. i just need to algorithm for it, if somebody show it with code it'll be more helpful of course..
function words($length, $prefix='') {
if ($length == 0) return;
foreach(range('a', 'z') as $letter) {
echo $prefix . $letter, "\n";
words($length-1, $prefix . $letter);
}
}
Usage:
words(10);
Try it here: http://codepad.org/zdTGLtjY (with words up to 3 letters)
Version 1:
for($s = 'a'; $s <= 'zzzzzzzzzz'; print $s++.PHP_EOL);
as noted by Paul in comments below, this will only go to zzzzzzzzyz. A bit slower (if anyone cares) but correct version would be:
//modified to include arnaud576875's method of checking exit condition
for($s = 'a'; !isset($s[10]); print $s++.PHP_EOL);
<?php
function makeWord($length, $prefix='')
{
if ($length <= 0)
{
echo $prefix . "\n";
return;
}
foreach(range('a', 'z') as $letter)
{
makeWord($length - 1, $prefix . $letter);
}
}
// Use the function to write the words.
$minSize = 1;
$maxSize = 3;
for ($i = $minSize; $i <= $maxSize; $i++)
{
makeWord($i);
}
?>

Check if a String Starts with a Number in PHP [duplicate]

This question already has answers here:
Closed 12 years ago.
The community reviewed whether to reopen this question 10 months ago and left it closed:
Original close reason(s) were not resolved
Possible Duplicate:
Check if a String Ends with a Number in PHP
I'm trying to implement the function below. Would it be best to use some type of regex here? I need to capture the number too.
function startsWithNumber($string) {
$startsWithNumber = false;
// Logic
return $startsWithNumber;
}
You can use substr and ctype_digit:
function startsWithNumber($string) {
return strlen($string) > 0 && ctype_digit(substr($string, 0, 1));
}
The additional strlen is just required as ctype_digit returns true for an empty string before PHP 5.1.
Or, if you rather want to use a regular expression:
function startsWithNumber($str) {
return preg_match('/^\d/', $str) === 1;
}
Something like to this may work to you:
function str2int($string) {
$length = strlen($string);
for ($i = 0, $int = ''; $i < $length; $i++) {
if (is_numeric($string[$i]))
$int .= $string[$i];
else break;
}
return (int) $int;
}

finding common prefix of array of strings

I have an array like this:
$sports = array(
'Softball - Counties',
'Softball - Eastern',
'Softball - North Harbour',
'Softball - South',
'Softball - Western'
);
I would like to find the longest common prefix of the string. In this instance, it would be 'Softball - '
I am thinking that I would follow this process
$i = 1;
// loop to the length of the first string
while ($i < strlen($sports[0]) {
// grab the left most part up to i in length
$match = substr($sports[0], 0, $i);
// loop through all the values in array, and compare if they match
foreach ($sports as $sport) {
if ($match != substr($sport, 0, $i) {
// didn't match, return the part that did match
return substr($sport, 0, $i-1);
}
} // foreach
// increase string length
$i++;
} // while
// if you got to here, then all of them must be identical
Questions
Is there a built in function or much simpler way of doing this ?
For my 5 line array that is probably fine, but if I were to do several thousand line arrays, there would be a lot of overhead, so I would have to be move calculated with my starting values of $i, eg $i = halfway of string, if it fails, then $i/2 until it works, then increment $i by 1 until we succeed. So that we are doing the least number of comparisons to get a result.
Is there a formula/algorithm out already out there for this kind of problem?
If you can sort your array, then there is a simple and very fast solution.
Simply compare the first item to the last one.
If the strings are sorted, any prefix common to all strings will be common to the sorted first and last strings.
sort($sport);
$s1 = $sport[0]; // First string
$s2 = $sport[count($sport)-1]; // Last string
$len = min(strlen($s1), strlen($s2));
// While we still have string to compare,
// if the indexed character is the same in both strings,
// increment the index.
for ($i=0; $i<$len && $s1[$i]==$s2[$i]; $i++);
$prefix = substr($s1, 0, $i);
I would use this:
$prefix = array_shift($array); // take the first item as initial prefix
$length = strlen($prefix);
// compare the current prefix with the prefix of the same length of the other items
foreach ($array as $item) {
// check if there is a match; if not, decrease the prefix by one character at a time
while ($length && substr($item, 0, $length) !== $prefix) {
$length--;
$prefix = substr($prefix, 0, -1);
}
if (!$length) {
break;
}
}
Update  
Here’s another solution, iteratively comparing each n-th character of the strings until a mismatch is found:
$pl = 0; // common prefix length
$n = count($array);
$l = strlen($array[0]);
while ($pl < $l) {
$c = $array[0][$pl];
for ($i=1; $i<$n; $i++) {
if ($array[$i][$pl] !== $c) break 2;
}
$pl++;
}
$prefix = substr($array[0], 0, $pl);
This is even more efficient as there are only at most numberOfStrings‍·‍commonPrefixLength atomic comparisons.
I implemented #diogoriba algorithm into code, with this result:
Finding the common prefix of the first two strings, and then comparing this with all following strings starting from the 3rd, and trim the common string if nothing common is found, wins in situations where there is more in common in the prefixes than different.
But bumperbox's original algorithm (except the bugfixes) wins where the strings have less in common in their prefix than different. Details in the code comments!
Another idea I implemented:
First check for the shortest string in the array, and use this for comparison rather than simply the first string. In the code, this is implemented with the custom written function arrayStrLenMin().
Can bring down iterations dramatically, but the function arrayStrLenMin() may itself cause ( more or less) iterations.
Simply starting with the length of first string in array seems quite clumsy, but may turn out effective, if arrayStrLenMin() needs many iterations.
Get the maximum common prefix of strings in an array with as little iterations as possible (PHP)
Code + Extensive Testing + Remarks:
function arrayStrLenMin ($arr, $strictMode = false, $forLoop = false) {
$errArrZeroLength = -1; // Return value for error: Array is empty
$errOtherType = -2; // Return value for error: Found other type (than string in array)
$errStrNone = -3; // Return value for error: No strings found (in array)
$arrLength = count($arr);
if ($arrLength <= 0 ) { return $errArrZeroLength; }
$cur = 0;
foreach ($arr as $key => $val) {
if (is_string($val)) {
$min = strlen($val);
$strFirstFound = $key;
// echo("Key\tLength / Notification / Error\n");
// echo("$key\tFound first string member at key with length: $min!\n");
break;
}
else if ($strictMode) { return $errOtherType; } // At least 1 type other than string was found.
}
if (! isset($min)) { return $errStrNone; } // No string was found in array.
// SpeedRatio of foreach/for is approximately 2/1 as dicussed at:
// http://juliusbeckmann.de/blog/php-foreach-vs-while-vs-for-the-loop-battle.html
// If $strFirstFound is found within the first 1/SpeedRatio (=0.5) of the array, "foreach" is faster!
if (! $forLoop) {
foreach ($arr as $key => $val) {
if (is_string($val)) {
$cur = strlen($val);
// echo("$key\t$cur\n");
if ($cur == 0) { return $cur; } // 0 is the shortest possible string, so we can abort here.
if ($cur < $min) { $min = $cur; }
}
// else { echo("$key\tNo string!\n"); }
}
}
// If $strFirstFound is found after the first 1/SpeedRatio (=0.5) of the array, "for" is faster!
else {
for ($i = $strFirstFound + 1; $i < $arrLength; $i++) {
if (is_string($arr[$i])) {
$cur = strlen($arr[$i]);
// echo("$i\t$cur\n");
if ($cur == 0) { return $cur; } // 0 is the shortest possible string, so we can abort here.
if ($cur < $min) { $min = $cur; }
}
// else { echo("$i\tNo string!\n"); }
}
}
return $min;
}
function strCommonPrefixByStr($arr, $strFindShortestFirst = false) {
$arrLength = count($arr);
if ($arrLength < 2) { return false; }
// Determine loop length
/// Find shortest string in array: Can bring down iterations dramatically, but the function arrayStrLenMin() itself can cause ( more or less) iterations.
if ($strFindShortestFirst) { $end = arrayStrLenMin($arr, true); }
/// Simply start with length of first string in array: Seems quite clumsy, but may turn out effective, if arrayStrLenMin() needs many iterations.
else { $end = strlen($arr[0]); }
for ($i = 1; $i <= $end + 1; $i++) {
// Grab the part from 0 up to $i
$commonStrMax = substr($arr[0], 0, $i);
echo("Match: $i\t$commonStrMax\n");
// Loop through all the values in array, and compare if they match
foreach ($arr as $key => $str) {
echo(" Str: $key\t$str\n");
// Didn't match, return the part that did match
if ($commonStrMax != substr($str, 0, $i)) {
return substr($commonStrMax, 0, $i-1);
}
}
}
// Special case: No mismatch (hence no return) happened until loop end!
return $commonStrMax; // Thus entire first common string is the common prefix!
}
function strCommonPrefixByChar($arr, $strFindShortestFirst = false) {
$arrLength = count($arr);
if ($arrLength < 2) { return false; }
// Determine loop length
/// Find shortest string in array: Can bring down iterations dramatically, but the function arrayStrLenMin() itself can cause ( more or less) iterations.
if ($strFindShortestFirst) { $end = arrayStrLenMin($arr, true); }
/// Simply start with length of first string in array: Seems quite clumsy, but may turn out effective, if arrayStrLenMin() needs many iterations.
else { $end = strlen($arr[0]); }
for ($i = 0 ; $i <= $end + 1; $i++) {
// Grab char $i
$char = substr($arr[0], $i, 1);
echo("Match: $i\t"); echo(str_pad($char, $i+1, " ", STR_PAD_LEFT)); echo("\n");
// Loop through all the values in array, and compare if they match
foreach ($arr as $key => $str) {
echo(" Str: $key\t$str\n");
// Didn't match, return the part that did match
if ($char != $str[$i]) { // Same functionality as ($char != substr($str, $i, 1)). Same efficiency?
return substr($arr[0], 0, $i);
}
}
}
// Special case: No mismatch (hence no return) happened until loop end!
return substr($arr[0], 0, $end); // Thus entire first common string is the common prefix!
}
function strCommonPrefixByNeighbour($arr) {
$arrLength = count($arr);
if ($arrLength < 2) { return false; }
/// Get the common string prefix of the first 2 strings
$strCommonMax = strCommonPrefixByChar(array($arr[0], $arr[1]));
if ($strCommonMax === false) { return false; }
if ($strCommonMax == "") { return ""; }
$strCommonMaxLength = strlen($strCommonMax);
/// Now start looping from the 3rd string
echo("-----\n");
for ($i = 2; ($i < $arrLength) && ($strCommonMaxLength >= 1); $i++ ) {
echo(" STR: $i\t{$arr[$i]}\n");
/// Compare the maximum common string with the next neighbour
/*
//// Compare by char: Method unsuitable!
// Iterate from string end to string beginning
for ($ii = $strCommonMaxLength - 1; $ii >= 0; $ii--) {
echo("Match: $ii\t"); echo(str_pad($arr[$i][$ii], $ii+1, " ", STR_PAD_LEFT)); echo("\n");
// If you find the first mismatch from the end, break.
if ($arr[$i][$ii] != $strCommonMax[$ii]) {
$strCommonMaxLength = $ii - 1; break;
// BUT!!! We may falsely assume that the string from the first mismatch until the begining match! This new string neighbour string is completely "unexplored land", there might be differing chars closer to the beginning. This method is not suitable. Better use string comparison than char comparison.
}
}
*/
//// Compare by string
for ($ii = $strCommonMaxLength; $ii > 0; $ii--) {
echo("MATCH: $ii\t$strCommonMax\n");
if (substr($arr[$i],0,$ii) == $strCommonMax) {
break;
}
else {
$strCommonMax = substr($strCommonMax,0,$ii - 1);
$strCommonMaxLength--;
}
}
}
return substr($arr[0], 0, $strCommonMaxLength);
}
// Tests for finding the common prefix
/// Scenarios
$filesLeastInCommon = array (
"/Vol/1/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/a/1",
"/Vol/2/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/a/2",
"/Vol/1/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/b/1",
"/Vol/1/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/b/2",
"/Vol/2/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/b/c/1",
"/Vol/2/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/a/1",
);
$filesLessInCommon = array (
"/Vol/1/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/a/1",
"/Vol/1/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/a/2",
"/Vol/1/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/b/1",
"/Vol/1/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/b/2",
"/Vol/2/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/b/c/1",
"/Vol/2/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/a/1",
);
$filesMoreInCommon = array (
"/Voluuuuuuuuuuuuuumes/1/a/a/1",
"/Voluuuuuuuuuuuuuumes/1/a/a/2",
"/Voluuuuuuuuuuuuuumes/1/a/b/1",
"/Voluuuuuuuuuuuuuumes/1/a/b/2",
"/Voluuuuuuuuuuuuuumes/2/a/b/c/1",
"/Voluuuuuuuuuuuuuumes/2/a/a/1",
);
$sameDir = array (
"/Volumes/1/a/a/",
"/Volumes/1/a/a/aaaaa/2",
);
$sameFile = array (
"/Volumes/1/a/a/1",
"/Volumes/1/a/a/1",
);
$noCommonPrefix = array (
"/Volumes/1/a/a/",
"/Volumes/1/a/a/aaaaa/2",
"Net/1/a/a/aaaaa/2",
);
$longestLast = array (
"/Volumes/1/a/a/1",
"/Volumes/1/a/a/aaaaa/2",
);
$longestFirst = array (
"/Volumes/1/a/a/aaaaa/1",
"/Volumes/1/a/a/2",
);
$one = array ("/Volumes/1/a/a/aaaaa/1");
$empty = array ( );
// Test Results for finding the common prefix
/*
I tested my functions in many possible scenarios.
The results, the common prefixes, were always correct in all scenarios!
Just try a function call with your individual array!
Considering iteration efficiency, I also performed tests:
I put echo functions into the functions where iterations occur, and measured the number of CLI line output via:
php <script with strCommonPrefixByStr or strCommonPrefixByChar> | egrep "^ Str:" | wc -l GIVES TOTAL ITERATION SUM.
php <Script with strCommonPrefixByNeighbour> | egrep "^ Str:" | wc -l PLUS | egrep "^MATCH:" | wc -l GIVES TOTAL ITERATION SUM.
My hypothesis was proven:
strCommonPrefixByChar wins in situations where the strings have less in common in their beginning (=prefix).
strCommonPrefixByNeighbour wins where there is more in common in the prefixes.
*/
// Test Results Table
// Used Functions | Iteration amount | Remarks
// $result = (strCommonPrefixByStr($filesLessInCommon)); // 35
// $result = (strCommonPrefixByChar($filesLessInCommon)); // 35 // Same amount of iterations, but much fewer characters compared because ByChar instead of ByString!
// $result = (strCommonPrefixByNeighbour($filesLessInCommon)); // 88 + 42 = 130 // Loses in this category!
// $result = (strCommonPrefixByStr($filesMoreInCommon)); // 137
// $result = (strCommonPrefixByChar($filesMoreInCommon)); // 137 // Same amount of iterations, but much fewer characters compared because ByChar instead of ByString!
// $result = (strCommonPrefixByNeighbour($filesLeastInCommon)); // 12 + 4 = 16 // Far the winner in this category!
echo("Common prefix of all members:\n");
var_dump($result);
// Tests for finding the shortest string in array
/// Arrays
// $empty = array ();
// $noStrings = array (0,1,2,3.0001,4,false,true,77);
// $stringsOnly = array ("one","two","three","four");
// $mixed = array (0,1,2,3.0001,"four",false,true,"seven", 8888);
/// Scenarios
// I list them from fewest to most iterations, which is not necessarily equivalent to slowest to fastest!
// For speed consider the remarks in the code considering the Speed ratio of foreach/for!
//// Fewest iterations (immediate abort on "Found other type", use "for" loop)
// foreach( array($empty, $noStrings, $stringsOnly, $mixed) as $arr) {
// echo("NEW ANALYSIS:\n");
// echo("Result: " . arrayStrLenMin($arr, true, true) . "\n\n");
// }
/* Results:
NEW ANALYSIS:
Result: Array is empty!
NEW ANALYSIS:
Result: Found other type!
NEW ANALYSIS:
Key Length / Notification / Error
0 Found first string member at key with length: 3!
1 3
2 5
3 4
Result: 3
NEW ANALYSIS:
Result: Found other type!
*/
//// Fewer iterations (immediate abort on "Found other type", use "foreach" loop)
// foreach( array($empty, $noStrings, $stringsOnly, $mixed) as $arr) {
// echo("NEW ANALYSIS:\n");
// echo("Result: " . arrayStrLenMin($arr, true, false) . "\n\n");
// }
/* Results:
NEW ANALYSIS:
Result: Array is empty!
NEW ANALYSIS:
Result: Found other type!
NEW ANALYSIS:
Key Length / Notification / Error
0 Found first string member at key with length: 3!
0 3
1 3
2 5
3 4
Result: 3
NEW ANALYSIS:
Result: Found other type!
*/
//// More iterations (No immediate abort on "Found other type", use "for" loop)
// foreach( array($empty, $noStrings, $stringsOnly, $mixed) as $arr) {
// echo("NEW ANALYSIS:\n");
// echo("Result: " . arrayStrLenMin($arr, false, true) . "\n\n");
// }
/* Results:
NEW ANALYSIS:
Result: Array is empty!
NEW ANALYSIS:
Result: No strings found!
NEW ANALYSIS:
Key Length / Notification / Error
0 Found first string member at key with length: 3!
1 3
2 5
3 4
Result: 3
NEW ANALYSIS:
Key Length / Notification / Error
4 Found first string member at key with length: 4!
5 No string!
6 No string!
7 5
8 No string!
Result: 4
*/
//// Most iterations (No immediate abort on "Found other type", use "foreach" loop)
// foreach( array($empty, $noStrings, $stringsOnly, $mixed) as $arr) {
// echo("NEW ANALYSIS:\n");
// echo("Result: " . arrayStrLenMin($arr, false, false) . "\n\n");
// }
/* Results:
NEW ANALYSIS:
Result: Array is empty!
NEW ANALYSIS:
Result: No strings found!
NEW ANALYSIS:
Key Length / Notification / Error
0 Found first string member at key with length: 3!
0 3
1 3
2 5
3 4
Result: 3
NEW ANALYSIS:
Key Length / Notification / Error
4 Found first string member at key with length: 4!
0 No string!
1 No string!
2 No string!
3 No string!
4 4
5 No string!
6 No string!
7 5
8 No string!
Result: 4
*/
Probably there is some terribly well-regarded algorithm for this, but just off the top of my head, if you know your commonality is going to be on the left-hand side like in your example, you could do way better than your posted methodology by first finding the commonality of the first two strings, and then iterating down the rest of the list, trimming the common string as necessary to achieve commonality or terminating with failure if you trim all the way to nothing.
I think you're on the right way. But instead of incrementing i when all of the string passes, you could do this:
1) Compare the first 2 strings in the array and find out how many common characters they have. Save the common characters in a separate string called maxCommon, for example.
2) Compare the third string w/ maxCommon. If the number of common characters is smaller, trim maxCommon to the characters that match.
3) Repeat and rinse for the rest of the array. At the end of the process, maxCommon will have the string that is common to all of the array elements.
This will add some overhead because you'll need to compare each string w/ maxCommon, but will drastically reduce the number of iterations you'll need to get your results.
I assume that by "common part" you mean "longest common prefix". That is a much simpler to compute than any common substring.
This cannot be done without reading (n+1) * m characters in the worst case and n * m + 1 in the best case, where n is the length of the longest common prefix and m is the number of strings.
Comparing one letter at a time achieves that efficiency (Big Theta (n * m)).
Your proposed algorithm runs in Big Theta(n^2 * m), which is much, much slower for large inputs.
The third proposed algorithm of finding the longest prefix of the first two strings, then comparing that with the third, fourth, etc. also has a running time in Big Theta(n * m), but with a higher constant factor. It will probably only be slightly slower in practice.
Overall, I would recommend just rolling your own function, since the first algorithm is too slow and the two others will be about equally complicated to write anyway.
Check out WikiPedia for a description of Big Theta notation.
Here's an elegant, recursive implementation in JavaScript:
function prefix(strings) {
switch (strings.length) {
case 0:
return "";
case 1:
return strings[0];
case 2:
// compute the prefix between the two strings
var a = strings[0],
b = strings[1],
n = Math.min(a.length, b.length),
i = 0;
while (i < n && a.charAt(i) === b.charAt(i))
++i;
return a.substring(0, i);
default:
// return the common prefix of the first string,
// and the common prefix of the rest of the strings
return prefix([ strings[0], prefix(strings.slice(1)) ]);
}
}
not that I know of
yes: instead of comparing the substring from 0 to length i, you can simply check the ith character (you already know that characters 0 to i-1 match).
Short and sweet version, perhaps not the most efficient:
/// Return length of longest common prefix in an array of strings.
function _commonPrefix($array) {
if(count($array) < 2) {
if(count($array) == 0)
return false; // empty array: undefined prefix
else
return strlen($array[0]); // 1 element: trivial case
}
$len = max(array_map('strlen',$array)); // initial upper limit: max length of all strings.
$prevval = reset($array);
while(($newval = next($array)) !== FALSE) {
for($j = 0 ; $j < $len ; $j += 1)
if($newval[$j] != $prevval[$j])
$len = $j;
$prevval = $newval;
}
return $len;
}
// TEST CASE:
$arr = array('/var/yam/yamyam/','/var/yam/bloorg','/var/yar/sdoo');
print_r($arr);
$plen = _commonprefix($arr);
$pstr = substr($arr[0],0,$plen);
echo "Res: $plen\n";
echo "==> ".$pstr."\n";
echo "dir: ".dirname($pstr.'aaaa')."\n";
Output of the test case:
Array
(
[0] => /var/yam/yamyam/
[1] => /var/yam/bloorg
[2] => /var/yar/sdoo
)
Res: 7
==> /var/ya
dir: /var
#bumperbox
Your basic code needed some correction to work in ALL scenarios!
Your loop only compares until one character before the last character!
The mismatch can possibly occur 1 loop cycle after the latest common character.
Hence you have to at least check until 1 character after your first string's last character.
Hence your comparison operator must be "<= 1" or "< 2".
Currently your algorithm fails
if the first string is completely included in all other strings,
or completely included in all other strings except the last character.
In my next answer/post, I will attach iteration optimized code!
Original Bumperbox code PLUS correction (PHP):
function shortest($sports) {
$i = 1;
// loop to the length of the first string
while ($i < strlen($sports[0])) {
// grab the left most part up to i in length
// REMARK: Culturally biased towards LTR writing systems. Better say: Grab frombeginning...
$match = substr($sports[0], 0, $i);
// loop through all the values in array, and compare if they match
foreach ($sports as $sport) {
if ($match != substr($sport, 0, $i)) {
// didn't match, return the part that did match
return substr($sport, 0, $i-1);
}
}
$i++; // increase string length
}
}
function shortestCorrect($sports) {
$i = 1;
while ($i <= strlen($sports[0]) + 1) {
// Grab the string from its beginning with length $i
$match = substr($sports[0], 0, $i);
foreach ($sports as $sport) {
if ($match != substr($sport, 0, $i)) {
return substr($sport, 0, $i-1);
}
}
$i++;
}
// Special case: No mismatch happened until loop end! Thus entire str1 is common prefix!
return $sports[0];
}
$sports1 = array(
'Softball',
'Softball - Eastern',
'Softball - North Harbour');
$sports2 = array(
'Softball - Wester',
'Softball - Western',
);
$sports3 = array(
'Softball - Western',
'Softball - Western',
);
$sports4 = array(
'Softball - Westerner',
'Softball - Western',
);
echo("Output of the original function:\n"); // Failure scenarios
var_dump(shortest($sports1)); // NULL rather than the correct 'Softball'
var_dump(shortest($sports2)); // NULL rather than the correct 'Softball - Wester'
var_dump(shortest($sports3)); // NULL rather than the correct 'Softball - Western'
var_dump(shortest($sports4)); // Only works if the second string is at least one character longer!
echo("\nOutput of the corrected function:\n"); // All scenarios work
var_dump(shortestCorrect($sports1));
var_dump(shortestCorrect($sports2));
var_dump(shortestCorrect($sports3));
var_dump(shortestCorrect($sports4));
How about something like this? It can be further optimised by not having to check the lengths of the strings if we can use the null terminating character (but I am assuming python strings have length cached somewhere?)
def find_common_prefix_len(strings):
"""
Given a list of strings, finds the length common prefix in all of them.
So
apple
applet
application
would return 3
"""
prefix = 0
curr_index = -1
num_strings = len(strings)
string_lengths = [len(s) for s in strings]
while True:
curr_index += 1
ch_in_si = None
for si in xrange(0, num_strings):
if curr_index >= string_lengths[si]:
return prefix
else:
if si == 0:
ch_in_si = strings[0][curr_index]
elif strings[si][curr_index] != ch_in_si:
return prefix
prefix += 1
I would use a recursive algorithm like this:
1 - get the first string in the array
2 - call the recursive prefix method with the first string as a param
3 - if prefix is empty return no prefix
4 - loop through all the strings in the array
4.1 - if any of the strings does not start with the prefix
4.1.1 - call recursive prefix method with prefix - 1 as a param
4.2 return prefix
// Common prefix
$common = '';
$sports = array(
'Softball T - Counties',
'Softball T - Eastern',
'Softball T - North Harbour',
'Softball T - South',
'Softball T - Western'
);
// find mini string
$minLen = strlen($sports[0]);
foreach ($sports as $s){
if($minLen > strlen($s))
$minLen = strlen($s);
}
// flag to break out of inner loop
$flag = false;
// The possible common string length does not exceed the minimum string length.
// The following solution is O(n^2), this can be improve.
for ($i = 0 ; $i < $minLen; $i++){
$tmp = $sports[0][$i];
foreach ($sports as $s){
if($s[$i] != $tmp)
$flag = true;
}
if($flag)
break;
else
$common .= $sports[0][$i];
}
print $common;
The solutions here work only for finding commonalities at the beginning of strings. Here is a function that looks for the longest common substring anywhere in an array of strings.
http://www.christopherbloom.com/2011/02/24/find-the-longest-common-substring-using-php/
The top answer seemed a bit long, so here's a concise solution with a runtime of O(n2).
function findLongestPrefix($arr) {
return array_reduce($arr, function($prefix, $item) {
$length = min(strlen($prefix), strlen($item));
while (substr($prefix, 0, $length) !== substr($item, 0, $length)) {
$length--;
}
return substr($prefix, 0, $length);
}, $arr[0]);
}
print findLongestPrefix($sports); // Softball -
For what it's worth, here's another alternative I came up with.
I used this for finding the common prefix for a list of products codes (ie. where there are multiple product SKUs that have a common series of characters at the start):
/**
* Try to find a common prefix for a list of strings
*
* #param array $strings
* #return string
*/
function findCommonPrefix(array $strings)
{
$prefix = '';
$chars = array_map("str_split", $strings);
$matches = call_user_func_array("array_intersect_assoc", $chars);
if ($matches) {
$i = 0;
foreach ($matches as $key => $value) {
if ($key != $i) {
unset($matches[$key]);
}
$i++;
}
$prefix = join('', $matches);
}
return $prefix;
}
This is an addition to the #Gumbo answer. If you want to ensure that the chosen, common prefix does not break words, use this. I am just having it look for a blank space at the end of the chosen string. If that exists we know that there was more to all of the phrases, so we truncate it.
function product_name_intersection($array){
$pl = 0; // common prefix length
$n = count($array);
$l = strlen($array[0]);
$first = current($array);
while ($pl < $l) {
$c = $array[0][$pl];
for ($i=1; $i<$n; $i++) {
if (!isset($array[$i][$pl]) || $array[$i][$pl] !== $c) break 2;
}
$pl++;
}
$prefix = substr($array[0], 0, $pl);
if ($pl < strlen($first) && substr($prefix, -1, 1) != ' ') {
$prefix = preg_replace('/\W\w+\s*(\W*)$/', '$1', $prefix);
}
$prefix = preg_replace('/^\W*(.+?)\W*$/', '$1', $prefix);
return $prefix;
}
Sharing a Typescript solution for this question. I split it into 2 methods, just to keep it clean while at it.
function longestCommonPrefix(strs: string[]): string {
let output = '';
if(strs.length > 0) {
output = strs[0];
if(strs.length > 1) {
for(let i=1; i <strs.length; i++) {
output = checkCommonPrefix(output, strs[i]);
}
}
}
return output;
};
function checkCommonPrefix(str1: string, str2: string): string {
let output = '';
let len = Math.min(str1.length, str2.length);
let i = 0;
while(i < len) {
if(str1[i] === str2[i]) {
output += str1[i];
} else {
i = len;
}
i++;
}
return output;
}

Categories