/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php

https://bitbucket.org/moodle/moodle · PHP · 198 lines · 136 code · 29 blank · 33 comment · 30 complexity · be5a7d790f37dd1d2612cbb1587c71e3 MD5 · raw file

  1. <?php
  2. namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
  3. use PhpOffice\PhpSpreadsheet\Calculation\Exception;
  4. use PhpOffice\PhpSpreadsheet\Calculation\Functions;
  5. use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch;
  6. use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
  7. class ExcelMatch
  8. {
  9. public const MATCHTYPE_SMALLEST_VALUE = -1;
  10. public const MATCHTYPE_FIRST_VALUE = 0;
  11. public const MATCHTYPE_LARGEST_VALUE = 1;
  12. /**
  13. * MATCH.
  14. *
  15. * The MATCH function searches for a specified item in a range of cells
  16. *
  17. * Excel Function:
  18. * =MATCH(lookup_value, lookup_array, [match_type])
  19. *
  20. * @param mixed $lookupValue The value that you want to match in lookup_array
  21. * @param mixed $lookupArray The range of cells being searched
  22. * @param mixed $matchType The number -1, 0, or 1. -1 means above, 0 means exact match, 1 means below.
  23. * If match_type is 1 or -1, the list has to be ordered.
  24. *
  25. * @return int|string The relative position of the found item
  26. */
  27. public static function MATCH($lookupValue, $lookupArray, $matchType = self::MATCHTYPE_LARGEST_VALUE)
  28. {
  29. $lookupArray = Functions::flattenArray($lookupArray);
  30. $lookupValue = Functions::flattenSingleValue($lookupValue);
  31. $matchType = ($matchType === null)
  32. ? self::MATCHTYPE_LARGEST_VALUE
  33. : (int) Functions::flattenSingleValue($matchType);
  34. try {
  35. // Input validation
  36. self::validateLookupValue($lookupValue);
  37. self::validateMatchType($matchType);
  38. self::validateLookupArray($lookupArray);
  39. $keySet = array_keys($lookupArray);
  40. if ($matchType == self::MATCHTYPE_LARGEST_VALUE) {
  41. // If match_type is 1 the list has to be processed from last to first
  42. $lookupArray = array_reverse($lookupArray);
  43. $keySet = array_reverse($keySet);
  44. }
  45. $lookupArray = self::prepareLookupArray($lookupArray, $matchType);
  46. } catch (Exception $e) {
  47. return $e->getMessage();
  48. }
  49. // MATCH() is not case sensitive, so we convert lookup value to be lower cased if it's a string type.
  50. if (is_string($lookupValue)) {
  51. $lookupValue = StringHelper::strToLower($lookupValue);
  52. }
  53. $valueKey = null;
  54. switch ($matchType) {
  55. case self::MATCHTYPE_LARGEST_VALUE:
  56. $valueKey = self::matchLargestValue($lookupArray, $lookupValue, $keySet);
  57. break;
  58. case self::MATCHTYPE_FIRST_VALUE:
  59. $valueKey = self::matchFirstValue($lookupArray, $lookupValue);
  60. break;
  61. case self::MATCHTYPE_SMALLEST_VALUE:
  62. default:
  63. $valueKey = self::matchSmallestValue($lookupArray, $lookupValue);
  64. }
  65. if ($valueKey !== null) {
  66. return ++$valueKey;
  67. }
  68. // Unsuccessful in finding a match, return #N/A error value
  69. return Functions::NA();
  70. }
  71. private static function matchFirstValue($lookupArray, $lookupValue)
  72. {
  73. $wildcardLookup = ((bool) preg_match('/([\?\*])/', $lookupValue));
  74. $wildcard = WildcardMatch::wildcard($lookupValue);
  75. foreach ($lookupArray as $i => $lookupArrayValue) {
  76. $typeMatch = ((gettype($lookupValue) === gettype($lookupArrayValue)) ||
  77. (is_numeric($lookupValue) && is_numeric($lookupArrayValue)));
  78. if (
  79. $typeMatch && is_string($lookupValue) &&
  80. $wildcardLookup && WildcardMatch::compare($lookupArrayValue, $wildcard)
  81. ) {
  82. // wildcard match
  83. return $i;
  84. } elseif ($lookupArrayValue === $lookupValue) {
  85. // exact match
  86. return $i;
  87. }
  88. }
  89. return null;
  90. }
  91. private static function matchLargestValue($lookupArray, $lookupValue, $keySet)
  92. {
  93. foreach ($lookupArray as $i => $lookupArrayValue) {
  94. $typeMatch = ((gettype($lookupValue) === gettype($lookupArrayValue)) ||
  95. (is_numeric($lookupValue) && is_numeric($lookupArrayValue)));
  96. if ($typeMatch && ($lookupArrayValue <= $lookupValue)) {
  97. return array_search($i, $keySet);
  98. }
  99. }
  100. return null;
  101. }
  102. private static function matchSmallestValue($lookupArray, $lookupValue)
  103. {
  104. $valueKey = null;
  105. // The basic algorithm is:
  106. // Iterate and keep the highest match until the next element is smaller than the searched value.
  107. // Return immediately if perfect match is found
  108. foreach ($lookupArray as $i => $lookupArrayValue) {
  109. $typeMatch = gettype($lookupValue) === gettype($lookupArrayValue);
  110. if ($lookupArrayValue === $lookupValue) {
  111. // Another "special" case. If a perfect match is found,
  112. // the algorithm gives up immediately
  113. return $i;
  114. } elseif ($typeMatch && $lookupArrayValue >= $lookupValue) {
  115. $valueKey = $i;
  116. } elseif ($typeMatch && $lookupArrayValue < $lookupValue) {
  117. //Excel algorithm gives up immediately if the first element is smaller than the searched value
  118. break;
  119. }
  120. }
  121. return $valueKey;
  122. }
  123. private static function validateLookupValue($lookupValue): void
  124. {
  125. // Lookup_value type has to be number, text, or logical values
  126. if ((!is_numeric($lookupValue)) && (!is_string($lookupValue)) && (!is_bool($lookupValue))) {
  127. throw new Exception(Functions::NA());
  128. }
  129. }
  130. private static function validateMatchType($matchType): void
  131. {
  132. // Match_type is 0, 1 or -1
  133. if (
  134. ($matchType !== self::MATCHTYPE_FIRST_VALUE) &&
  135. ($matchType !== self::MATCHTYPE_LARGEST_VALUE) && ($matchType !== self::MATCHTYPE_SMALLEST_VALUE)
  136. ) {
  137. throw new Exception(Functions::NA());
  138. }
  139. }
  140. private static function validateLookupArray($lookupArray): void
  141. {
  142. // Lookup_array should not be empty
  143. $lookupArraySize = count($lookupArray);
  144. if ($lookupArraySize <= 0) {
  145. throw new Exception(Functions::NA());
  146. }
  147. }
  148. private static function prepareLookupArray($lookupArray, $matchType)
  149. {
  150. // Lookup_array should contain only number, text, or logical values, or empty (null) cells
  151. foreach ($lookupArray as $i => $value) {
  152. // check the type of the value
  153. if ((!is_numeric($value)) && (!is_string($value)) && (!is_bool($value)) && ($value !== null)) {
  154. throw new Exception(Functions::NA());
  155. }
  156. // Convert strings to lowercase for case-insensitive testing
  157. if (is_string($value)) {
  158. $lookupArray[$i] = StringHelper::strToLower($value);
  159. }
  160. if (
  161. ($value === null) &&
  162. (($matchType == self::MATCHTYPE_LARGEST_VALUE) || ($matchType == self::MATCHTYPE_SMALLEST_VALUE))
  163. ) {
  164. unset($lookupArray[$i]);
  165. }
  166. }
  167. return $lookupArray;
  168. }
  169. }