I have an array of string values which sometimes form repeating value patterns ('a', 'b', 'c', 'd')
$array = array(
'a', 'b', 'c', 'd',
'a', 'b', 'c', 'd',
'c', 'd',
);
I would like to find duplicate patterns based on the array order and group them by that same order (to maintain it).
$patterns = array(
array('number' => 2, 'values' => array('a', 'b', 'c', 'd')),
array('number' => 1, 'values' => array('c'))
array('number' => 1, 'values' => array('d'))
);
Notice that [a,b], [b,c], & [c,d] are not patterns by themselves because they are inside the larger [a,b,c,d] pattern and the last [c,d] set only appears once so it's not a pattern either - just the individual values 'c' and 'd'
Another example:
$array = array(
'x', 'x', 'y', 'x', 'b', 'x', 'b', 'a'
//[.......] [.] [[......] [......]] [.]
);
which produces
$patterns = array(
array('number' => 2, 'values' => array('x')),
array('number' => 1, 'values' => array('y')),
array('number' => 2, 'values' => array('x', 'b')),
array('number' => 1, 'values' => array('a'))
);
How can I do this?
Character arrays are just strings. Regex is the king of string pattern matching. Add recursion and the solution is pretty elegant, even with the conversion back and forth from character arrays:
function findPattern($str){
$results = array();
if(is_array($str)){
$str = implode($str);
}
if(strlen($str) == 0){ //reached the end
return $results;
}
if(preg_match_all('/^(.+)\1+(.*?)$/',$str,$matches)){ //pattern found
$results[] = array('number' => (strlen($str) - strlen($matches[2][0])) / strlen($matches[1][0]), 'values' => str_split($matches[1][0]));
return array_merge($results,findPattern($matches[2][0]));
}
//no pattern found
$results[] = array('number' => 1, 'values' => array(substr($str, 0, 1)));
return array_merge($results,findPattern(substr($str, 1)));
}
You can test here : https://eval.in/507818 and https://eval.in/507815
If c and d can be grouped, this is my code:
<?php
$array = array(
'a', 'b', 'c', 'd',
'a', 'b', 'c', 'd',
'c', 'd',
);
$res = array();
foreach ($array AS $value) {
if (!isset($res[$value])) {
$res[$value] = 0;
}
$res[$value]++;
}
foreach ($res AS $key => $value) {
$fArray[$value][] = $key;
for ($i = $value - 1; $i > 0; $i--) {
$fArray[$i][] = $key;
}
}
$res = array();
foreach($fArray AS $key => $value) {
if (!isset($res[serialize($value)])) {
$res[serialize($value)] = 0;
}
$res[serialize($value)]++;
}
$fArray = array();
foreach($res AS $key => $value) {
$fArray[] = array('number' => $value, 'values' => unserialize($key));
}
echo '<pre>';
var_dump($fArray);
echo '</pre>';
Final result is:
array (size=2)
0 =>
array (size=2)
'number' => int 2
'values' =>
array (size=4)
0 => string 'a' (length=1)
1 => string 'b' (length=1)
2 => string 'c' (length=1)
3 => string 'd' (length=1)
1 =>
array (size=2)
'number' => int 1
'values' =>
array (size=2)
0 => string 'c' (length=1)
1 => string 'd' (length=1)
The following code will return the expected result, finding the longest portions with repeated values:
function pepito($array) {
$sz=count($array);
$patterns=Array();
for ($pos=0;$pos<$sz;$pos+=$len) {
$nb=1;
for ($len=floor($sz/2);$len>0;$len--) {
while (array_slice($array, $pos, $len)==array_slice($array, $pos+$len, $len)) {
$pos+=$len;
$nb++;
}
if ($nb>1) break;
}
if (!$len) $len=1;
$patterns[]=Array('number'=>$nb, 'values'=>array_slice($array, $pos, $len));
}
return $patterns;
}
This will match with your examples:
{['a', 'b', 'c', 'd'], ['a', 'b', 'c', 'd']}, ['c', 'd']
or {['x'], ['x']}, ['y'], {['x', 'b'], ['x', 'b']}, ['a']
The difficult part is more about examples like:
{['one', 'one', 'two'], ['one', 'one', 'two']}
Or the most difficult choice to make:
one, two, one, two, one, two, one, two
Because we can group this in both the form:
[one, two], [one, two], [one, two], [one, two]
[one, two, one, two], [one, two, one, two]
where there is no "evident" choice. My above algorithm will always consider the longest match, as this is the most easy implementation to consider any combination.
EDIT: You should also consider cases where the longest match is after a shorter one:
Example:
'one', 'two', 'one', 'two', 'three', 'four', 'one', 'two', 'three', 'four'
If you start from left to right, you might want to group as :
{['one', 'two'], ['one', 'two'],} 'three', 'four', 'one', 'two', 'three', 'four'
when you could group like:
'one', 'two', {['one', 'two', 'three', 'four'], ['one', 'two', 'three', 'four']}
This situation has to be resolved with recursive calls to get the better solution but this will result in longer execution time:
function pepito($array) {
if (($sz=count($array))<1) return Array();
$pos=0;
$nb=1;
for ($len=floor($sz/2);$len>0;$len--) {
while (array_slice($array, $pos, $len)==array_slice($array, $pos+$len, $len)) {
$pos+=$len;
$nb++;
}
if ($nb>1) break;
}
if (!$len) $len=1;
$rec1=pepito(array_slice($array, $pos+$len));
$rec2=pepito(array_slice($array, 1));
if (count($rec1)<count($rec2)+1) {
return array_merge(Array(Array('number'=>$nb, 'values'=>array_slice($array, $pos, $len))), $rec1);
}
return array_merge(Array(Array('number'=>1, 'values'=>array_slice($array, 0, 1))), $rec2);
}
Definitions:
Pattern base: The sequence of elements that repeat within a pattern. (ie. For [a,b,a,b,c], [a,b] is the pattern base and [a,b,a,b] is the pattern.
We want to start searching for the longest pattern base, followed by the next longest, and so forth. It's important to understand that if we find a pattern, we don't need to check within it for the start of another pattern with a base of the same length.
Here's the proof.
Assume that A is a pattern base, and that we've encountered the pattern AA. Assume that B is a another pattern base, of the same length, that forms a pattern starting within A. Let Y be the overlapping elements. If A=XY, then AA=XYXY. Since B is the same length, it must be the case that B=YX because in order to complete B we must use the remaining elements in A. Moreover, since B forms a pattern we must have BB, which is YXYX. Since A starts before B, we have XYXYX=AAX=XBB. If B repeats again, we would have XBBB=XYXYXYX=AAAX. Therefore, B cannot repeat an additional time without A repeating an additional time. Thus, we do not need to check for longer patterns within those generated by A.
The longest pattern possible consists of half the elements in the whole list because the simplest pattern can occur exactly twice. Thus, we can start checking for patterns of half of the length and work our way down to patterns of size 2.
Assuming that we search the array from left to right, if a pattern is found, we only need to search on either side of it for additional patterns. To the left, there are no patterns with bases of the same length, or they would have been discovered beforehand. Thus, we search the left side for patterns using the next smallest base size. The elements to the right of the pattern haven't been searched so we continue searching for patterns using a base of the same size.
The function to do this is as follows:
function get_patterns($arr, $len = null) {
// The smallest pattern base length for which a pattern can be found
$minlen = 2;
// No pattern base length was specified
if ($len === null) {
// Use the longest pattern base length possible
$maxlen = floor(count($arr) / 2);
return get_patterns($arr, $maxlen);
// Base length is too small to find any patterns
} else if ($len < $minlen) {
// Compile elements into lists consisting of one element
$results = array();
$num = 1;
$elem = $arr[0];
for ($i=1; $i < count($arr); $i++) {
if ($elem === $arr[$i]) {
$num++;
} else {
array_push($results, array(
'number' => $num,
'values' => array( $elem )
));
$num = 1;
$elem = $arr[$i];
}
}
array_push($results, array(
'number' => $num,
'values' => array( $elem )
));
return $results;
}
// Cycle through elements until there aren't enough elements to fit
// another repition.
for ($i=0; $i < count($arr) - $len * 2 + 1; $i++) {
// Number of times pattern base occurred
$num_read = 1; // One means there is no pattern yet
// Current pattern base we are attempting to match against
$base = array_slice($arr, $i, $len);
// Check for matches using segments of the same length for the elements
// following the current pattern base
for ($j = $i + $len; $j < count($arr) - $len + 1; $j += $len) {
// Elements being compared to pattern base
$potential_match = array_slice($arr, $j, $len);
// Match found
if (has_same_elements($base, $potential_match)) {
$num_read++;
// NO match found
} else {
// Do not check again using currently selected elements
break;
}
}
// Patterns were encountered
if ($num_read > 1) {
// The total number of elements that make up the pattern
$pattern_len = $num_read * $len;
// The elements before the pattern
$before = array_slice($arr, 0, $i);
// The elements after the pattern
$after = array_slice(
$arr, $i + $pattern_len, count($arr) - $pattern_len - $i
);
$results = array_merge(
// Patterns of a SMALLER length may exist beforehand
count($before) > 0 ? get_patterns($before, $len-1) : array(),
// Patterns that were found
array(
array(
'number' => $num_read,
'values' => $base
)
),
// Patterns of the SAME length may exist afterward
count($after) > 0 ? get_patterns($after, $len) : array()
);
return $results;
}
}
// No matches were encountered
// Search for SMALLER patterns
return get_patterns($arr, $len-1);
}
The function has_same_elements, which was used to check if arrays with primitive keys are identical, is as follows:
// Returns true if two arrays have the same elements.
//
// Precondition: Elements must be primitive data types (ie. int, string, etc)
function has_same_elements($a1, $a2) {
// There are a different number of elements
if (count($a1) != count($a2)) {
return false;
}
for ($i=0; $i < count($a1); $i++) {
if ($a1[$i] !== $a2[$i]) {
return false;
}
}
return true;
}
In order to speed up the code, there are a few things that you could do. Instead of slicing the array, you can supply the function with indexes to the start and end position that are to be examined, along with the array. Also, using strings may be slow so you can create an array that maps strings to numbers and vice versa. Then you can convert the array of strings into an array of numbers and use it instead. After you get the result, you can convert the arrays of numbers back into strings.
I tested the function using the following code:
$tests = array(
'a,b,c,d',
'a',
'a,a,a,a',
'a,a,a,a,a',
'a,a,a,a,a,a',
'b,a,a,a,a,c',
'b,b,a,a,a,a,c,c',
'b,b,a,a,d,a,a,c,c',
'a,b,c,d,a,b,c,d,c,d',
'x,x,y,x,b,x,b,a'
);
echo '<pre>';
foreach ($tests as $test) {
echo '<div>';
$arr = explode(',',$test);
echo "$test<br /><br />";
pretty_print(get_patterns($arr));
echo '</div><br />';
}
echo '</pre>';
The function that I used to print the output, pretty_print is as follows:
function pretty_print($results) {
foreach ($results as $result) {
$a = "array('" . implode("','", $result['values']) . "')";
echo "array('number' => ${result['number']}, 'values' => $a)<br />";
}
}
The output from the test code is as follows:
a,b,c,d
array('number' => 1, 'values' => array('a'))
array('number' => 1, 'values' => array('b'))
array('number' => 1, 'values' => array('c'))
array('number' => 1, 'values' => array('d'))
a
array('number' => 1, 'values' => array('a'))
a,a,a,a
array('number' => 2, 'values' => array('a','a'))
a,a,a,a,a
array('number' => 2, 'values' => array('a','a'))
array('number' => 1, 'values' => array('a'))
a,a,a,a,a,a
array('number' => 2, 'values' => array('a','a','a'))
b,a,a,a,a,c
array('number' => 1, 'values' => array('b'))
array('number' => 2, 'values' => array('a','a'))
array('number' => 1, 'values' => array('c'))
b,b,a,a,a,a,c,c
array('number' => 2, 'values' => array('b'))
array('number' => 2, 'values' => array('a','a'))
array('number' => 2, 'values' => array('c'))
b,b,a,a,d,a,a,c,c
array('number' => 2, 'values' => array('b'))
array('number' => 2, 'values' => array('a'))
array('number' => 1, 'values' => array('d'))
array('number' => 2, 'values' => array('a'))
array('number' => 2, 'values' => array('c'))
a,b,c,d,a,b,c,d,c,d
array('number' => 2, 'values' => array('a','b','c','d'))
array('number' => 1, 'values' => array('c'))
array('number' => 1, 'values' => array('d'))
x,x,y,x,b,x,b,a
array('number' => 2, 'values' => array('x'))
array('number' => 1, 'values' => array('y'))
array('number' => 2, 'values' => array('x','b'))
array('number' => 1, 'values' => array('a'))
OK, here is my take, the code below splits the whole original array into longest adjacent non-overlapping chunks.
So in the situation like this
'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'c', 'd'
[ ] [ ] [ ] [ ] <-- use 2 long groups
[ ] [ ] [ ] [ ] [ ] [ ] <-- and not 4 short
it will prefer 2 long groups to 4 shorter groups.
Update: also tested with examples from another answer, works for these cases too:
one, two, one, two, one, two, one, two
[one two one two], [one two one two]
'one' 'two' 'one' 'two' 'three' 'four' 'one' 'two' 'three' 'four'
['one'] ['two'] ['one' 'two' 'three' 'four'] ['one' 'two' 'three' 'four']
Here is the code and tests:
<?php
/*
* Splits an $array into chunks of $chunk_size.
* Returns number of repeats, start index and chunk which has
* max number of ajacent repeats.
*/
function getRepeatCount($array, $chunk_size) {
$parts = array_chunk($array, $chunk_size);
$maxRepeats = 1;
$maxIdx = 0;
$repeats = 1;
$len = count($parts);
for ($i = 0; $i < $len-1; $i++) {
if ($parts[$i] === $parts[$i+1]) {
$repeats += 1;
if ($repeats > $maxRepeats) {
$maxRepeats = $repeats;
$maxIdx = $i - ($repeats-2);
}
} else {
$repeats = 1;
}
}
return array($maxRepeats, $maxIdx*$chunk_size, $parts[$maxIdx]);
}
/*
* Finds longest pattern in the $array.
* Returns number of repeats, start index and pattern itself.
*/
function findLongestPattern($array) {
$len = count($array);
for ($window = floor($len/2); $window >= 1; $window--) {
$num_chunks = ceil($len/$window);
for ($i = 0; $i < $num_chunks; $i++) {
list($repeats, $idx, $pattern) = getRepeatCount(
array_slice($array, $i), $window
);
if ($repeats > 1) {
return array($repeats, $idx+$i, $pattern);
}
}
}
return array(1, 0, [$array[0]]);
}
/*
* Splits $array into longest adjacent non-overlapping parts.
*/
function splitToPatterns($array) {
if (count($array) < 1) {
return $array;
}
list($repeats, $start, $pattern) = findLongestPattern($array);
$end = $start + count($pattern) * $repeats;
return array_merge(
splitToPatterns(array_slice($array, 0, $start)),
array(
array('number'=>$repeats, 'values' => $pattern)
),
splitToPatterns(array_slice($array, $end))
);
}
Tests:
function isEquals($expected, $actual) {
$exp_str = json_encode($expected);
$act_str = json_encode($actual);
$equals = $exp_str === $act_str;
if (!$equals) {
echo 'Equals check failed'.PHP_EOL;
echo 'expected: '.$exp_str.PHP_EOL;
echo 'actual : '.$act_str.PHP_EOL;
}
return $equals;
}
assert(isEquals(
array(1, 0, ['a']), getRepeatCount(['a','b','c'], 1)
));
assert(isEquals(
array(1, 0, ['a']), getRepeatCount(['a','b','a','b','c'], 1)
));
assert(isEquals(
array(2, 0, ['a','b']), getRepeatCount(['a','b','a','b','c'], 2)
));
assert(isEquals(
array(1, 0, ['a','b','a']), getRepeatCount(['a','b','a','b','c'], 3)
));
assert(isEquals(
array(3, 0, ['a','b']), getRepeatCount(['a','b','a','b','a','b','a'], 2)
));
assert(isEquals(
array(2, 2, ['a','c']), getRepeatCount(['x','c','a','c','a','c'], 2)
));
assert(isEquals(
array(1, 0, ['x','c','a']), getRepeatCount(['x','c','a','c','a','c'], 3)
));
assert(isEquals(
array(2, 0, ['a','b','c','d']),
getRepeatCount(['a','b','c','d','a','b','c','d','c','d'],4)
));
assert(isEquals(
array(2, 2, ['a','c']), findLongestPattern(['x','c','a','c','a','c'])
));
assert(isEquals(
array(1, 0, ['a']), findLongestPattern(['a','b','c'])
));
assert(isEquals(
array(2, 2, ['c','a']),
findLongestPattern(['a','b','c','a','c','a'])
));
assert(isEquals(
array(2, 0, ['a','b','c','d']),
findLongestPattern(['a','b','c','d','a','b','c','d','c','d'])
));
// Find longest adjacent non-overlapping patterns
assert(isEquals(
array(
array('number'=>1, 'values'=>array('a')),
array('number'=>1, 'values'=>array('b')),
array('number'=>1, 'values'=>array('c')),
),
splitToPatterns(['a','b','c'])
));
assert(isEquals(
array(
array('number'=>1, 'values'=>array('a')),
array('number'=>1, 'values'=>array('b')),
array('number'=>2, 'values'=>array('c','a')),
),
splitToPatterns(['a','b','c','a','c','a'])
));
assert(isEquals(
array(
array('number'=>2, 'values'=>array('a','b','c','d')),
array('number'=>1, 'values'=>array('c')),
array('number'=>1, 'values'=>array('d')),
),
splitToPatterns(['a','b','c','d','a','b','c','d','c','d'])
));
/* 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'c', 'd', */
/* [ ] [ ] [ ] [ ] */
/* NOT [ ] [ ] [ ] [ ] [ ] [ ] */
assert(isEquals(
array(
array('number'=>2, 'values'=>array('a','b','a','b')),
array('number'=>1, 'values'=>array('c')),
array('number'=>1, 'values'=>array('d')),
),
splitToPatterns(['a','b','a','b','a','b','a','b','c','d'])
));
/* 'x', 'x', 'y', 'x', 'b', 'x', 'b', 'a' */
/* // [ ] [ ] [ ] [ ] [ ] [ ] */
assert(isEquals(
array(
array('number'=>2, 'values'=>array('x')),
array('number'=>1, 'values'=>array('y')),
array('number'=>2, 'values'=>array('x','b')),
array('number'=>1, 'values'=>array('a')),
),
splitToPatterns(['x','x','y','x','b','x','b','a'])
));
// one, two, one, two, one, two, one, two
// [ ] [ ]
assert(isEquals(
array(
array('number'=>2, 'values'=>array('one', 'two', 'one', 'two')),
),
splitToPatterns(['one', 'two', 'one', 'two', 'one', 'two', 'one', 'two'])
));
// 'one', 'two', 'one', 'two', 'three', 'four', 'one', 'two', 'three', 'four'
// [ ] [ ] [ ] [ ]
assert(isEquals(
array(
array('number'=>1, 'values'=>array('one')),
array('number'=>1, 'values'=>array('two')),
array('number'=>2, 'values'=>array('one','two','three','four')),
),
splitToPatterns(['one', 'two', 'one', 'two', 'three', 'four', 'one', 'two', 'three','four'])
));
/* 'a', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'c', */
/* [ ] [ ] [ ] [ ] */
assert(isEquals(
array(
array('number'=>1, 'values'=>array('a')),
array('number'=>2, 'values'=>array('a','b','a','b')),
array('number'=>1, 'values'=>array('c')),
),
splitToPatterns(['a','a','b','a','b','a','b','a','b','c'])
));
/* 'a', 'b', 'a', 'b', 'c', 'd', 'a', 'b', 'a', 'b', 'a', 'b' */
// [ ] [ ] [ ] [ ] [ ] [ ] [ ]
assert(isEquals(
array(
array('number'=>2, 'values'=>array('a', 'b')),
array('number'=>1, 'values'=>array('c')),
array('number'=>1, 'values'=>array('d')),
array('number'=>3, 'values'=>array('a','b')),
),
splitToPatterns(['a', 'b', 'a', 'b', 'c', 'd', 'a', 'b', 'a', 'b', 'a', 'b'])
));
/* 'a', 'c', 'd', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'c', */
/* [ ] [ ] [ ] [ ] [ ] [ ] */
assert(isEquals(
array(
array('number'=>1, 'values'=>array('a')),
array('number'=>2, 'values'=>array('a','b','a','b')),
array('number'=>1, 'values'=>array('c')),
),
splitToPatterns(['a','a','b','a','b','a','b','a','b','c'])
));
I started with this now but at the end my brain burn and I don't know where to start to compare the arrays... Enjoy!
$array = array(
'x', 'x', 'y', 'x', 'b', 'x', 'b', 'a'
//[.......] [.] [[......] [......]] [.]
);
$arrayCount = count($array);
$res = array();
for($i = 0; $i < $arrayCount; $i++) {
for($j = 1; $j < $arrayCount; $j++) {
$res[$i][] = array_slice($array, $i, $j);
}
}
//echo '<pre>';
//var_dump($res);
//echo '</pre>';
//
//die;
$resCount = count($res);
$oneResCount = count($res[0]);
First make a function which will find the possible group matches in the array for a given group array, starting from a specific index in the array and will return the number of matches found.
function findGroupMatch($group, $array, $startFrom) {
$match = 0;
while($group == array_slice($array, $startFrom, count($group))) {
$match++;
$startFrom += count($group);
}
return $match;
}
Now, we need to iterate through each item to find possible groups and then send it to findGroupMatch() function to check if any match for the group exists in the next items. The trick to find a possible group is finding an item which matches any of the previous items. If so, we find a possible group taking all the previous items starting from the matched item. Otherwise we just increase the list of unmatched items and at the end we enter all unmatched items as single item groups. (In the given example, we have a, b, c, d, a.... When we find 2nd a in the array, it matches the previous a, So, we consider a, b, c, d a possible group and send it to function findGroupMatch(), to check how many more groups we can find in the next items.)
$array = array(
'a', 'b', 'c', 'd',
'a', 'b', 'c', 'd',
'c', 'd',
);
$unMatchedItems = array();
$totalCount = count($array);
$groupsArray = array();
for($i=0; $i < $totalCount; $i++) {
$item = $array[$i];
if(in_array($item, $unMatchedItems)) {
$matched_keys = array_keys($unMatchedItems, $item);
foreach($matched_keys as $key) {
$possibleGroup = array_slice($unMatchedItems, $key);
$matches = findGroupMatch($possibleGroup, $array, $i);
if ($matches) {
//Insert the items before group as single item group
if ($key > 0) {
for ($j = 0; $j < $key; $j++) {
$groupsArray[] = array('number' => 1, 'values' => array($unMatchedItems[$j]));
}
}
//Insert the group array
$groupsArray[] = array('number' => $matches + 1, 'values' => $possibleGroup); //number includes initial group also so $matches + 1
$i += (count($possibleGroup) * $matches) - 1; //skip the matched items from next iteration
//Empty the unmatched array to start with a new group search
$unMatchedItems = array();
break;
}
}
//If there was no matches, add the item to the unMatched group
if(!$matches) $unMatchedItems[] = $item;
} else {
$unMatchedItems[] = $item;
}
}
//Insert the remaining items as single item group
for($k=0; $k<count($unMatchedItems); $k++) {
$groupsArray[] = array('number' => 1, 'values' => array($unMatchedItems[$k]));
}
print_r($groupsArray);
The Result will be like this: (Check this PHP Fiddle for testing and also https://eval.in/507333 for your another input test.)
Array
(
[0] => Array
(
[number] => 2
[values] => Array
(
[0] => a
[1] => b
[2] => c
[3] => d
)
)
[1] => Array
(
[number] => 1
[values] => Array
(
[0] => c
)
)
[2] => Array
(
[number] => 1
[values] => Array
(
[0] => d
)
)
)
The first example is super easy with recursion. The second example... not so easy.
The example below works for only the first example by assuming no pattern should ever contain two of the same element. This will also handle all individual element patterns at the end of the original array and keep the pattern order (of the first pattern occurrence).
function find_pattern($input, &$result) {
$values = []; // currently processed elements
$pattern = ''; // the current element pattern
$dupe_found = false; // did we find a duplicate element?
// search the values for the first that matches a previous value
while ($next = array_shift($input)) {
// check if the element was already found
if (in_array($next, $values)) {
// re-add the value back into the input, since the next call needs it
array_unshift($input, $next);
// add the resulting pattern
add_pattern($pattern, $values, $result);
// find the next pattern with a recursive call
find_pattern($input, $result);
// a duplicate element was found!
$dupe_found = true;
// the rest of the values are handled by recursion, break the while loop
break;
} else {
// not already found, so store the element and keep going
$values[] = $next;
// use the element to produce a key for the result set
$pattern .= $next;
}
}
// if no duplicate was found, then each value should be an individual pattern
if (!$dupe_found) {
foreach ($values as $value) {
add_pattern($value, [$value], $result);
}
}
}
function add_pattern($pattern, $values, &$result) {
// increment the pattern count
$result[$pattern]['number'] = isset($result[$pattern]['number']) ?
result[$pattern]['number']+1 : 1;
// add the current pattern to the result, if not already done
if (!isset($result[$pattern]['values'])) {
$result[$pattern]['values'] = $values;
}
}
And an example usage:
$input = [
'a', 'b', 'c', 'd',
'a', 'b', 'c', 'd',
'c', 'd'
];
$result = [];
find_pattern($input, $result);
echo "<pre>";
print_r($result);
echo "</pre>";
The example output:
Array
(
[abcd] => Array
(
[number] => 2
[values] => Array
(
[0] => a
[1] => b
[2] => c
[3] => d
)
)
[c] => Array
(
[number] => 1
[values] => Array
(
[0] => c
)
)
[d] => Array
(
[number] => 1
[values] => Array
(
[0] => d
)
)
)
You could do something like this:
<?php
$array = array(
'a', 'b', 'c', 'd',
'a', 'b', 'c', 'd',
'c', 'd'
);
// Call this function to get your patterns
function patternMatching(array $array) {
$patterns = array();
$belongsToPattern = array_fill(0, count($array), false);
// Find biggest patterns first
for ($size = (int) (count($array) / 2); $size > 0; $size--) {
// for each pattern: start at every possible point in the array
for($start=0; $start <= count($array) - $size; $start++) {
$p = findPattern($array, $start, $size);
if($p != null) {
/* Before we can save the pattern we need to check, if we've found a
* pattern that does not collide with patterns we've already found */
$hasConflict = false;
foreach($p["positions"] as $idx => $pos) {
$PatternConflicts = array_slice($belongsToPattern, $pos, $p["size"]);
$hasConflict = $hasConflict || in_array(true, $PatternConflicts);
}
if(!$hasConflict) {
/* Since we have found a pattern, we don't want to find more
* patterns for these positions */
foreach($p["positions"] as $idx => $pos) {
$replace = array_fill($pos, $p["size"], true);
$belongsToPattern = array_replace($belongsToPattern, $replace);
}
$patterns[] = $p;
// or only return number and values:
// $patterns[] = [ "number" => $p["number"], "values" => $p["values"]];
}
}
}
}
return $patterns;
}
function findPattern(array $haystack, $patternStart, $patternSize ) {
$size = count($haystack);
$patternCandidate = array_slice($haystack, $patternStart, $patternSize);
$patternCount = 1;
$patternPositions = [$patternStart];
for($i = $patternStart + $patternSize; $i <= $size - $patternSize; $i++) {
$patternCheck = array_slice($haystack, $i, $patternSize);
$diff = array_diff($patternCandidate, $patternCheck);
if(empty($diff)) {
$patternCount++;
$patternPositions[] = $i;
}
}
if($patternCount > 1 || $patternSize <= 1) {
return [
"number" => $patternCount,
"values" => $patternCandidate,
// Additional information needed for filtering, sorting, etc.
"positions" => $patternPositions,
"size" => $patternSize
];
} else {
return null;
}
}
$patterns = patternMatching($array);
print "<pre>";
print_r($patterns);
print "</pre>";
?>
The code might be far from being optimal in speed but it should do what you want to do for any sequence of strings in an array. patternMatching() returns the patterns ordered descending in size of the pattern and ascending by first occurence (You can use ['positions'][0] as a sorting criteria to achieve a different order).
This should do it:
<?php
$array = array(
'x', 'y', 'x', 'y', 'a',
'ab', 'c', 'd',
'a', 'b', 'c', 'd',
'c', 'd', 'x', 'y', 'b',
'x', 'y', 'b', 'c', 'd'
);
// convert the array to a string
$string = '';
foreach ($array as $a) {
$l = strlen($a)-1;
$string .= ($l) ? str_replace('::',':',$a[0] . ':' . substr($a,1,$l-1) . ':' . $a[$l]) . '-' : $a . '-';
}
// find patterns
preg_match_all('/(?=((.+)(?:.*?\2)+))/s', $string, $matches, PREG_SET_ORDER);
foreach ($matches as $m) {
$temp = str_replace('--','-',$m[2].'-');
$patterns[] = ($temp[0]==='-') ? substr($temp,1) : $temp;
}
// remove empty values and duplicates
$patterns = array_keys(array_flip(array_filter($patterns)));
// sort patterns
foreach ($patterns as $p) {
$sorted[$p] = strlen($p);
}
arsort($sorted);
// find double or more occurences
$stringClone = $string;
foreach ($sorted as $s=>$n) {
$nA = substr_count($stringClone,':'.$s);
$nZ = substr_count($stringClone,$s.':');
$number = substr_count($stringClone,$s);
$sub = explode('-',substr($stringClone,strpos($stringClone,$s),$n-1));
$values = $sub;
if($nA>0 || $nZ>0){
$numberAdjusted = $number - $nA - $nZ;
if($numberAdjusted > 1) {
$temp = '';
while($n--){
$temp .= '#';
}
$position = strpos(str_replace(':'.$s,':'.$temp,str_replace($s.':',$temp.':',$string)),$s);
$stringClone = str_replace(':'.$s,':'.$temp,$stringClone);
$stringClone = str_replace($s.':',$temp.':',$stringClone);
$result['p'.sprintf('%09d', $position)] = array('number'=>$numberAdjusted,'values'=>$values);
$stringClone = str_replace($s,'',$stringClone);
$stringClone = str_replace($temp,$s,$stringClone);
}
} else if($number>1){
$position = strpos($string,$s);
$result['p'.sprintf('%09d', $position)] = array('number'=>$number,'values'=>$values);
$stringClone = str_replace($s,'',$stringClone);
}
}
// add the remaining items
$remaining = array_flip(explode('-',substr($stringClone,0,-1)));
foreach ($remaining as $r=>$n) {
$position = strpos($string,$r);
$result['p'.sprintf('%09d', $position)] = array('number'=>1,'values'=>str_replace(':','',$r));
}
// sort results
ksort($result);
$result = array_values($result);
print_r($result);
?>
Working example here.
I need to transform ordinary numbers to Roman numerals with php and I have this code:
<?php
function roman2number($roman){
$conv = array(
array("letter" => 'I', "number" => 1),
array("letter" => 'V', "number" => 5),
array("letter" => 'X', "number" => 10),
array("letter" => 'L', "number" => 50),
array("letter" => 'C', "number" => 100),
array("letter" => 'D', "number" => 500),
array("letter" => 'M', "number" => 1000),
array("letter" => 0, "number" => 0)
);
$arabic = 0;
$state = 0;
$sidx = 0;
$len = strlen($roman);
while ($len >= 0) {
$i = 0;
$sidx = $len;
while ($conv[$i]['number'] > 0) {
if (strtoupper(#$roman[$sidx]) == $conv[$i]['letter']) {
if ($state > $conv[$i]['number']) {
$arabic -= $conv[$i]['number'];
} else {
$arabic += $conv[$i]['number'];
$state = $conv[$i]['number'];
}
}
$i++;
}
$len--;
}
return($arabic);
}
function number2roman($num,$isUpper=true) {
$n = intval($num);
$res = '';
/*** roman_numerals array ***/
$roman_numerals = array(
'M' => 1000,
'CM' => 900,
'D' => 500,
'CD' => 400,
'C' => 100,
'XC' => 90,
'L' => 50,
'XL' => 40,
'X' => 10,
'IX' => 9,
'V' => 5,
'IV' => 4,
'I' => 1
);
foreach ($roman_numerals as $roman => $number)
{
/*** divide to get matches ***/
$matches = intval($n / $number);
/*** assign the roman char * $matches ***/
$res .= str_repeat($roman, $matches);
/*** substract from the number ***/
$n = $n % $number;
}
/*** return the res ***/
if($isUpper) return $res;
else return strtolower($res);
}
/* TEST */
echo $s=number2roman(6,true);
echo "\n and bacK:\n";
echo roman2number($s);
?>
try this way but does not work:
echo $s=number2roman((.$row['id'].),true);
echo "\n and bacK:\n";
echo roman2number($s);
the problem is that I need to change numbers are readings of my sql database and do not know how to, from and through.
I found this code here: http://php.net/manual/en/function.base-convert.php
Optimized and prettified function:
/**
* #param int $number
* #return string
*/
function numberToRomanRepresentation($number) {
$map = array('M' => 1000, 'CM' => 900, 'D' => 500, 'CD' => 400, 'C' => 100, 'XC' => 90, 'L' => 50, 'XL' => 40, 'X' => 10, 'IX' => 9, 'V' => 5, 'IV' => 4, 'I' => 1);
$returnValue = '';
while ($number > 0) {
foreach ($map as $roman => $int) {
if($number >= $int) {
$number -= $int;
$returnValue .= $roman;
break;
}
}
}
return $returnValue;
}
Another way to do that
<?php
function ConverToRoman($num){
$n = intval($num);
$res = '';
//array of roman numbers
$romanNumber_Array = array(
'M' => 1000,
'CM' => 900,
'D' => 500,
'CD' => 400,
'C' => 100,
'XC' => 90,
'L' => 50,
'XL' => 40,
'X' => 10,
'IX' => 9,
'V' => 5,
'IV' => 4,
'I' => 1);
foreach ($romanNumber_Array as $roman => $number){
//divide to get matches
$matches = intval($n / $number);
//assign the roman char * $matches
$res .= str_repeat($roman, $matches);
//substract from the number
$n = $n % $number;
}
// return the result
return $res;
}
echo ConverToRoman(23);
?>
function rome($N){
$c='IVXLCDM';
for($a=5,$b=$s='';$N;$b++,$a^=7)
for($o=$N%$a,$N=$N/$a^0;$o--;$s=$c[$o>2?$b+$N-($N&=-2)+$o=1:$b].$s);
return $s;
}
// from polish wiki
CHECKED AND VERIFIED BY PHP UNIT
Make a class having name RomanNumerials and add a protected static property as defined:
protected static $lookup = [
1000 => 'M',
900 => 'CM',
500 => 'D',
400 => 'CD',
100 => 'C',
90 => 'XC',
50 => 'L',
40 => 'XL',
10 => 'X',
9 => 'IX',
5 => 'V',
4 => 'IV',
1 => 'I',
];
then add a method as follows
public function output ($number)
{
$solution = '';
foreach(static::$lookup as $limit => $glyph){
while ($number >= $limit) {
$solution .= $glyph;
$number -= $limit;
}
}
return $solution;
}
You can format integers into Roman numeral symbols with the ICU intl library's NumberFormatter class by setting the locale parameter to #numbers=roman with decimal style format:
function intToRomanNumeral(int $num) {
static $nf = new NumberFormatter('#numbers=roman', NumberFormatter::DECIMAL);
return $nf->format($num);
}
Output examples:
echo intToRomanNumeral(2); // II
echo intToRomanNumeral(5); // V
echo intToRomanNumeral(10); // X
echo intToRomanNumeral(50); // L
echo intToRomanNumeral(57); // LVII
echo intToRomanNumeral(58); // LVIII
echo intToRomanNumeral(100); // C
echo intToRomanNumeral(150); // CL
echo intToRomanNumeral(1000); // M
echo intToRomanNumeral(10000); // ↂ
Alternatively, you can use the MessageFormatter class, but in my own testing it appears to have significantly lower performance than NumberFormatter:
function intToRomanNumeral(int $num) {
static $nf = new MessageFormatter('#numbers=roman', '{0, number}');
return $nf->format([$num]);
}
I improve rome() function of Jasiek
function rome2($N)
{
// support for numbers greater than a thousand
$ss = '';
while ($N > 1000) {
$ss .= 'M';
$N -= 1000;
}
$c = 'IVXLCDM';
for ($a = 5, $b = 0, $s = ''; $N; $b++, $a ^= 7)
for ($o = $N % $a, $N = $N / $a ^ 0; $o--; ) {
$s = $c[$o > 2 ? $b + $N - ($N &= -2) + $o = 1 : $b] . $s;
}
return $ss.$s;
}
My own function it has the best performance:
function romanNumber($n)
{
// support for numbers greater than a thousand
$ret1 = '';
while ($n >= 1000) {
$ret1 .= 'M';
$n -= 1000;
}
$ret = '';
if ($n > 0) {
$n = (string) $n;
$l = 'IVXLCDM';
$j = 0; // goes by roman letters
for ($i = strlen($n)-1; $i >= 0; --$i) { // goes by decimal number
switch ($n[$i]) {
case 0: $s = ''; break;
case 1: $s = $l[$j]; break;
case 2: $s = $l[$j].$l[$j]; break;
case 3: $s = $l[$j].$l[$j].$l[$j]; break;
case 4: $s = $l[$j].$l[$j+1]; break;
case 5: $s = $l[$j+1]; break;
case 6: $s = $l[$j+1].$l[$j]; break;
case 7: $s = $l[$j+1].$l[$j].$l[$j]; break;
case 8: $s = $l[$j+1].$l[$j].$l[$j].$l[$j]; break;
case 9: $s = $l[$j].$l[$j+2]; break;
}
$j += 2;
$ret = $s.$ret;
}
}
return $ret1.$ret;
}
While appreciate the succinct mathematical nested loop approach posted by #user2095686, I agree more with #HaiderLasani's approach of writing the foreach() as the outer loop because it never unnecessarily revisits previously processed elements in the translation array. Haider's answer does not explain why this adjustment is ideal. I've created a demo to echo out all of the unnecessary conditional checks that #user2095686's answer makes. Gone one step further than #Haider's answer, I've written a condition inside of the while() loop to break both loops as soon as the modified input integer is reduced to zero.
Code: (Demo)
function numberToRoman(int $integer): string {
static $conversions = [
1000 => 'M',
900 => 'CM',
500 => 'D',
400 => 'CD',
100 => 'C',
90 => 'XC',
50 => 'L',
40 => 'XL',
10 => 'X',
9 => 'IX',
5 => 'V',
4 => 'IV',
1 => 'I'
];
$romanString = '';
foreach ($conversions as $int => $roman) {
while ($integer >= $int) {
$integer -= $int;
$romanString .= $roman;
if (!$integer) {
break 2;
}
}
}
return $romanString;
}
If you are not turned off by the style of performing assignment arithmetic inside of a conditional expression, the script can be condensed a little more...
foreach ($conversions as $int => $roman) {
while ($integer >= $int) {
$romanString .= $roman;
if (!($integer -= $int)) {
break 2;
}
}
}
Check out my solution here https://github.com/frostymarvelous/Whisppa-Libs/blob/master/Misc/Numeralo.php . It works both ways.
<?php
/**
* #package Whisppa
* #subpackage Misc
* #license http://opensource.org/licenses/MIT MIT License
* #author Stefan (frostymarvelous) Froelich <sfroelich#gmail.com>
* #copyright Copyright (c) 2015, Stefan (frostymarvelous) Froelich
*/
namespace Whisppa\Misc;
/**
* This class allows you to convert from Roman numerals to natural numbers and vice versa.
* I decided to do this as a fun challenge after reading http://thedailywtf.com/articles/Roman-Enumeration
* Took me about 30 minutes to come up with, research and code the solution.
* It can convert numbers up to 3,999,999 because I couldn't find any numerals for 5,000,000 above.
* Due to my inability to get the correct accented characters 5000 above, I resulted to using the pipe (|) to represent accent.
*/
class Numeralo
{
/**
* #var string[] A notation map to represent the common Roman numeral values.
* #static
*/
protected static $NOTATION =
[
'|', //one
'[', //five
']', //ten
];
/**
* #var \ArrayObject[] A map of Roman numerals based on place value. Each item ends with the first numeral in the next place value.
* #static
*/
protected static $NUMERALS_BY_PLACE_VALUE =
[
['I', 'V', 'X',], //ones
['X', 'L', 'C',], //tens
['C', 'D', 'M',], // hundreds
['M', 'V|', 'X|',], //thousands
['X|', 'L|', 'C|',], //tens of thousands
['C|', 'D|', 'M|',], //hundreds of thousands
['M|', '~', '~',], // millions. there are no values for the last two that I could find
];
/**
* #var string[] sA map of numbers and their representative Roman numerals in notation format. This map allows us to make any numeral by replacing the the notation with the place value equivalent.
* #static
*/
protected static $NUMBER_TO_NOTATION =
[
'0' => '',
'1' => '|',
'2' => '||',
'3' => '|||',
'4' => '|[',
'5' => '[',
'6' => '[|',
'7' => '[||',
'8' => '[|||',
'9' => '|]',
];
/**
* #var int[] A map of the major Roman numerals and the number equivalent.
* #static
*/
protected static $NUMERALS_TO_NUMBER =
[
'I' => 1,
'V' => 5,
'X' => 10,
'L' => 50,
'C' => 100,
'D' => 500,
'M' => 1000,
'V|' => 5000,
'X|' => 10000,
'L|' => 50000,
'C|' => 100000,
'D|' => 500000,
'M|' => 1000000,
];
/**
* Converts natural numbers to Roman numerals.
*
* #static
* #param int|string $number a number or numeric string less than 3,999,999
* #throws \InvalidArgumentException if the provided $number argument is not numeric or greater than 3,999,999.
* #return string Roman numeral equivalent
*/
public static function number_to_numerals($number) {
if(!is_numeric($number))
throw new \InvalidArgumentException('Only numbers allowed');
if($number > 3999999)
throw new \InvalidArgumentException('Number cannot be greater than 3,999,999');
$numerals = '';
$number_string = strrev((string) $number);
$length = strlen($number_string);
for($i = 0; $i < $length; $i++) {
$char = $number_string[$i];
$num_map = self::$NUMERALS_BY_PLACE_VALUE[$i];
$numerals = str_replace(self::$NOTATION, $num_map, self::$NUMBER_TO_NOTATION[$char]) . $numerals;
}
return $numerals;
}
/**
* Converts Roman numerals to natural numbers.
*
* #static
* #param string $numerals the Roman numerals to be converted
* #throws \InvalidArgumentException if the provided $numerals argument contains invalid characters.
* #return int the equivalent number
*/
public static function numerals_to_number($numerals) {
$number = 0;
$numeral_string = strrev((string) $numerals);
$length = strlen($numeral_string);
$prev_number = false;
$is_accented = false;
for($i = 0; $i < $length; $i++) {
$char = $numeral_string[$i];
if($char == '|') //check if it is an accent character
{
$is_accented = true;
continue;//skip this iteration and process it in the next one as the accent applies to the next char
}
else if($is_accented)
{
$char .= '|';
$is_accented = false;
}
//TODO Make a check using maybe regex at the beginning of the method.
if(!isset(self::$NUMERALS_TO_NUMBER[$char]))
throw new \InvalidArgumentException("Invalid character '{$char}' in numeral string");
$num = self::$NUMERALS_TO_NUMBER[$char];
//this is where the magic happens
//if the previous number divided by 5 or 10 is equal to the current number, then we subtract eg. 9 = IX. I = 1, X = 10, 10/10 = 1
if($prev_number)
{
if(($prev_number / 5) == $num || ($prev_number / 10) == $num)
$number -= $num;
else
$number += $num;
}
else
$number += $num;
$prev_number = $num;
}
return $number;
}
}
Using PHP, I'd like to convert a string containing a Roman number into its integer representation. I need this because I need to make calculations on them.
Wikipedia on Roman numerals
It would suffice to only recognize the basic Roman numeral characters, like:
$roman_values=array(
'I' => 1,
'V' => 5,
'X' => 10,
'L' => 50,
'C' => 100,
'D' => 500,
'M' => 1000,
);
That means the highest possible number is 3999 (MMMCMXCIX). I will use N to represent zero, other than that only positive integers are supported.
I cannot use the PEAR library for Roman numbers.
I found this great question on SO on how to test whether the string contains a valid Roman numeral:
How do you match only valid roman numerals with a regular expression?
What would be the best way of coding this?
How about this:
$romans = array(
'M' => 1000,
'CM' => 900,
'D' => 500,
'CD' => 400,
'C' => 100,
'XC' => 90,
'L' => 50,
'XL' => 40,
'X' => 10,
'IX' => 9,
'V' => 5,
'IV' => 4,
'I' => 1,
);
$roman = 'MMMCMXCIX';
$result = 0;
foreach ($romans as $key => $value) {
while (strpos($roman, $key) === 0) {
$result += $value;
$roman = substr($roman, strlen($key));
}
}
echo $result;
which should output 3999 for the supplied $roman. It seems to work for my limited testing:
MCMXC = 1990
MM = 2000
MMXI = 2011
MCMLXXV = 1975
You might want to do some validation first as well :-)
I am not sure whether you've got ZF or not, but in case you (or any of you who's reading this) do here is my snippet:
$number = new Zend_Measure_Number('MCMLXXV', Zend_Measure_Number::ROMAN);
$number->convertTo (Zend_Measure_Number::DECIMAL);
echo $number->getValue();
Zend_Measure_Number on framework.zend.com
This is the one I came up with, I added the validity check as well.
class RomanNumber {
//array of roman values
public static $roman_values=array(
'I' => 1, 'V' => 5,
'X' => 10, 'L' => 50,
'C' => 100, 'D' => 500,
'M' => 1000,
);
//values that should evaluate as 0
public static $roman_zero=array('N', 'nulla');
//Regex - checking for valid Roman numerals
public static $roman_regex='/^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/';
//Roman numeral validation function - is the string a valid Roman Number?
static function IsRomanNumber($roman) {
return preg_match(self::$roman_regex, $roman) > 0;
}
//Conversion: Roman Numeral to Integer
static function Roman2Int ($roman) {
//checking for zero values
if (in_array($roman, self::$roman_zero)) {
return 0;
}
//validating string
if (!self::IsRomanNumber($roman)) {
return false;
}
$values=self::$roman_values;
$result = 0;
//iterating through characters LTR
for ($i = 0, $length = strlen($roman); $i < $length; $i++) {
//getting value of current char
$value = $values[$roman[$i]];
//getting value of next char - null if there is no next char
$nextvalue = !isset($roman[$i + 1]) ? null : $values[$roman[$i + 1]];
//adding/subtracting value from result based on $nextvalue
$result += (!is_null($nextvalue) && $nextvalue > $value) ? -$value : $value;
}
return $result;
}
}
Quick idea - go through the Roman number from right to left, if value of $current (more to the left) is smaller than $previous, then subtract it from the result, if larger, then add it.
$romanValues=array(
'I' => 1,
'V' => 5,
'X' => 10,
'L' => 50,
'C' => 100,
'D' => 500,
'M' => 1000,
);
$roman = 'MMMCMXCIX';
// RTL
$arabic = 0;
$prev = null;
for ( $n = strlen($roman) - 1; $n >= 0; --$n ) {
$curr = $roman[$n];
if ( is_null($prev) ) {
$arabic += $romanValues[$roman[$n]];
} else {
$arabic += $romanValues[$prev] > $romanValues[$curr] ? -$romanValues[$curr] : +$romanValues[$curr];
}
$prev = $curr;
}
echo $arabic, "\n";
// LTR
$arabic = 0;
$romanLength = strlen($roman);
for ( $n = 0; $n < $romanLength; ++$n ) {
if ( $n === $romanLength - 1 ) {
$arabic += $romanValues[$roman[$n]];
} else {
$arabic += $romanValues[$roman[$n]] < $romanValues[$roman[$n+1]] ? -$romanValues[$roman[$n]] : +$romanValues[$roman[$n]];
}
}
echo $arabic, "\n";
Some validation of roman number should also be added, though you said that you already have found how to do it.
Copyrights is for this blog (btw!)
http://scriptsense.blogspot.com/2010/03/php-function-number-to-roman-and-roman.html
<?php
function roman2number($roman){
$conv = array(
array("letter" => 'I', "number" => 1),
array("letter" => 'V', "number" => 5),
array("letter" => 'X', "number" => 10),
array("letter" => 'L', "number" => 50),
array("letter" => 'C', "number" => 100),
array("letter" => 'D', "number" => 500),
array("letter" => 'M', "number" => 1000),
array("letter" => 0, "number" => 0)
);
$arabic = 0;
$state = 0;
$sidx = 0;
$len = strlen($roman);
while ($len >= 0) {
$i = 0;
$sidx = $len;
while ($conv[$i]['number'] > 0) {
if (strtoupper(#$roman[$sidx]) == $conv[$i]['letter']) {
if ($state > $conv[$i]['number']) {
$arabic -= $conv[$i]['number'];
} else {
$arabic += $conv[$i]['number'];
$state = $conv[$i]['number'];
}
}
$i++;
}
$len--;
}
return($arabic);
}
function number2roman($num,$isUpper=true) {
$n = intval($num);
$res = '';
/*** roman_numerals array ***/
$roman_numerals = array(
'M' => 1000,
'CM' => 900,
'D' => 500,
'CD' => 400,
'C' => 100,
'XC' => 90,
'L' => 50,
'XL' => 40,
'X' => 10,
'IX' => 9,
'V' => 5,
'IV' => 4,
'I' => 1
);
foreach ($roman_numerals as $roman => $number)
{
/*** divide to get matches ***/
$matches = intval($n / $number);
/*** assign the roman char * $matches ***/
$res .= str_repeat($roman, $matches);
/*** substract from the number ***/
$n = $n % $number;
}
/*** return the res ***/
if($isUpper) return $res;
else return strtolower($res);
}
/* TEST */
echo $s=number2roman(1965,true);
echo "\n and bacK:\n";
echo roman2number($s);
?>
I'm late to the party, but here's mine. Assumes valid Numerals in the string, but doesn't test for a valid Roman number, whatever that is...there doesn't seem to be a consensus. This function will work for Roman numbers like VC (95), or MIM (1999), or MMMMMM (6000).
function roman2dec( $roman ) {
$numbers = array(
'I' => 1,
'V' => 5,
'X' => 10,
'L' => 50,
'C' => 100,
'D' => 500,
'M' => 1000,
);
$roman = strtoupper( $roman );
$length = strlen( $roman );
$counter = 0;
$dec = 0;
while ( $counter < $length ) {
if ( ( $counter + 1 < $length ) && ( $numbers[$roman[$counter]] < $numbers[$roman[$counter + 1]] ) ) {
$dec += $numbers[$roman[$counter + 1]] - $numbers[$roman[$counter]];
$counter += 2;
} else {
$dec += $numbers[$roman[$counter]];
$counter++;
}
}
return $dec;
}
Whew! Those are quite a few answers, and made of them are code-heavy! How about we define an algorithm for this first, before I give an answer?
The Basics
Don't store multi-digit Roman numerals, like 'CM' => 900, or anything like that in an array. If you know that M - C (1000 - 100) equals 900, then ultimately, you should only be storing the values of 1000 and 100. You wouldn't have multi-digit Roman numerals like CMI for 901, would you? Any answer that does this will be inefficient from one that understands the Roman syntax.
The Algorithm
Example: LIX (59)
Do a for loop on the numbers, starting at the end of the string of Roman numerals. In our example: We start on "X".
Greater-Than-Equal-To Case — If the value we are looking at is the same or greater than the last value, simply add it to a cumulative result. In our example: $result += $numeral_values["X"].
Less-Than Case — If the value we are subtracting is less than the previous number, we subtract it from our cumulative result. In our example IX, I is 1 and X is 10, so, since 1 is less than 10, we subtract it: giving us 9.
The Demo
Full Working Demo Online
The Code
function RomanNumeralValues() {
return [
'I'=>1,
'V'=>5,
'X'=>10,
'L'=>50,
'C'=>100,
'D'=>500,
'M'=>1000,
];
}
function ConvertRomanNumeralToArabic($input_roman){
$input_length = strlen($input_roman);
if($input_length === 0) {
return $result;
}
$roman_numerals = RomanNumeralValues();
$current_pointer = 1;
$result = 0;
for($i = $input_length - 1; $i > -1; $i--){
$letter = $input_roman[$i];
$letter_value = $roman_numerals[$letter];
if($letter_value === $current_pointer) {
$result += $letter_value;
} elseif ($letter_value < $current_pointer) {
$result -= $letter_value;
} else {
$result += $letter_value;
$current_pointer = $letter_value;
}
}
return $result;
}
print ConvertRomanNumeralToArabic("LIX");
function romanToInt($s) {
$array = ["I"=>1,"V"=>5,"X"=>10,"L"=>50,"C"=>100,"D"=>500,"M"=>1000];
$sum = 0;
for ($i = 0; $i < strlen($s); $i++){
$curr = $s[$i];
$next = $s[$i+1];
if ($array[$curr] < $array[$next]) {
$sum += $array[$next] - $array[$curr];
$i++;
} else {
$sum += $array[$curr];
}
}
return $sum;
}
Define your own schema! (optional)
function rom2arab($rom,$letters=array()){
if(empty($letters)){
$letters=array('M'=>1000,
'D'=>500,
'C'=>100,
'L'=>50,
'X'=>10,
'V'=>5,
'I'=>1);
}else{
arsort($letters);
}
$arab=0;
foreach($letters as $L=>$V){
while(strpos($rom,$L)!==false){
$l=$rom[0];
$rom=substr($rom,1);
$m=$l==$L?1:-1;
$arab += $letters[$l]*$m;
}
}
return $arab;
}
Inspired by andyb's answer
I just wrote this in about 10 mins, it's not perfect, but seems to work for the few test cases I've given it. I'm not enforcing what values are allowed to be subtracted from what, this is just a basic loop that compares the current letter value with the next one in the sequence (if it exists) and then either adds the value or adds the subtracted amount to the total:
$roman = strtolower($_GET['roman']);
$values = array(
'i' => 1,
'v' => 5,
'x' => 10,
'l' => 50,
'c' => 100,
'd' => 500,
'm' => 1000,
);
$total = 0;
for($i=0; $i<strlen($roman); $i++)
{
$v = $values[substr($roman, $i, 1)];
$v2 = ($i < strlen($roman))?$values[substr($roman, $i+1, 1)]:0;
if($v2 && $v < $v2)
{
$total += ($v2 - $v);
$i++;
}
else
$total += $v;
}
echo $total;
Just stumbled across this beauty and have to post it all over:
function roman($N)
{
$c = 'IVXLCDM';
for ($a = 5, $b = $s = ''; $N; $b++, $a ^= 7)
{
for (
$o = $N % $a, $N = $N / $a ^ 0;
$o--;
$s = $c[$o > 2 ? $b + $N - ($N &= -2) + $o = 1 : $b] . $s
);
}
return $s;
}
function Romannumeraltonumber($input_roman){
$di=array('I'=>1,
'V'=>5,
'X'=>10,
'L'=>50,
'C'=>100,
'D'=>500,
'M'=>1000);
$result=0;
if($input_roman=='') return $result;
//LTR
for($i=0;$i<strlen($input_roman);$i++){
$result=(($i+1)<strlen($input_roman) and
$di[$input_roman[$i]]<$di[$input_roman[$i+1]])?($result-$di[$input_roman[$i]])
:($result+$di[$input_roman[$i]]);
}
return $result;
}
function rom_to_arabic($number) {
$symbols = array(
'M' => 1000,
'D' => 500,
'C' => 100,
'L' => 50,
'X' => 10,
'V' => 5,
'I' => 1);
$a = str_split($number);
$i = 0;
$temp = 0;
$value = 0;
$q = count($a);
while($i < $q) {
$thys = $symbols[$a[$i]];
if(isset($a[$i +1])) {
$next = $symbols[$a[$i +1]];
} else {
$next = 0;
}
if($thys < $next) {
$value -= $thys;
} else {
$value += $thys;
}
$temp = $thys;
$i++;
}
return $value;
}
function parseRomanNumerals($input)
{
$roman_val = '';
$roman_length = strlen($input);
$result_roman = 0;
for ($x = 0; $x <= $roman_length; $x++) {
$roman_val_prev = $roman_val;
$roman_numeral = substr($input, $roman_length-$x,1);
switch ($roman_numeral) {
case "M":
$roman_val = 1000;
break;
case "D":
$roman_val = 500;
break;
case "C":
$roman_val = 100;
break;
case "L":
$roman_val = 50;
break;
case "X":
$roman_val = 10;
break;
case "V":
$roman_val = 5;
break;
case "I":
$roman_val = 1;
break;
default:
$roman_val = 0;
}
if ($roman_val_prev<$roman_val) {
$result_roman = $result_roman - $roman_val;
}
else {
$result_roman = $result_roman + $roman_val;
}
}
return abs($result_roman);
}