Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]

   1  <?php
   2  
   3  namespace PhpOffice\PhpSpreadsheet\Calculation\TextData;
   4  
   5  use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
   6  use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp;
   7  use PhpOffice\PhpSpreadsheet\Calculation\Functions;
   8  use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
   9  use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
  10  
  11  class Extract
  12  {
  13      use ArrayEnabled;
  14  
  15      /**
  16       * LEFT.
  17       *
  18       * @param mixed $value String value from which to extract characters
  19       *                         Or can be an array of values
  20       * @param mixed $chars The number of characters to extract (as an integer)
  21       *                         Or can be an array of values
  22       *
  23       * @return array|string The joined string
  24       *         If an array of values is passed for the $value or $chars arguments, then the returned result
  25       *            will also be an array with matching dimensions
  26       */
  27      public static function left($value, $chars = 1)
  28      {
  29          if (is_array($value) || is_array($chars)) {
  30              return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $chars);
  31          }
  32  
  33          try {
  34              $value = Helpers::extractString($value);
  35              $chars = Helpers::extractInt($chars, 0, 1);
  36          } catch (CalcExp $e) {
  37              return $e->getMessage();
  38          }
  39  
  40          return mb_substr($value ?? '', 0, $chars, 'UTF-8');
  41      }
  42  
  43      /**
  44       * MID.
  45       *
  46       * @param mixed $value String value from which to extract characters
  47       *                         Or can be an array of values
  48       * @param mixed $start Integer offset of the first character that we want to extract
  49       *                         Or can be an array of values
  50       * @param mixed $chars The number of characters to extract (as an integer)
  51       *                         Or can be an array of values
  52       *
  53       * @return array|string The joined string
  54       *         If an array of values is passed for the $value, $start or $chars arguments, then the returned result
  55       *            will also be an array with matching dimensions
  56       */
  57      public static function mid($value, $start, $chars)
  58      {
  59          if (is_array($value) || is_array($start) || is_array($chars)) {
  60              return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $start, $chars);
  61          }
  62  
  63          try {
  64              $value = Helpers::extractString($value);
  65              $start = Helpers::extractInt($start, 1);
  66              $chars = Helpers::extractInt($chars, 0);
  67          } catch (CalcExp $e) {
  68              return $e->getMessage();
  69          }
  70  
  71          return mb_substr($value ?? '', --$start, $chars, 'UTF-8');
  72      }
  73  
  74      /**
  75       * RIGHT.
  76       *
  77       * @param mixed $value String value from which to extract characters
  78       *                         Or can be an array of values
  79       * @param mixed $chars The number of characters to extract (as an integer)
  80       *                         Or can be an array of values
  81       *
  82       * @return array|string The joined string
  83       *         If an array of values is passed for the $value or $chars arguments, then the returned result
  84       *            will also be an array with matching dimensions
  85       */
  86      public static function right($value, $chars = 1)
  87      {
  88          if (is_array($value) || is_array($chars)) {
  89              return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $chars);
  90          }
  91  
  92          try {
  93              $value = Helpers::extractString($value);
  94              $chars = Helpers::extractInt($chars, 0, 1);
  95          } catch (CalcExp $e) {
  96              return $e->getMessage();
  97          }
  98  
  99          return mb_substr($value ?? '', mb_strlen($value ?? '', 'UTF-8') - $chars, $chars, 'UTF-8');
 100      }
 101  
 102      /**
 103       * TEXTBEFORE.
 104       *
 105       * @param mixed $text the text that you're searching
 106       *                    Or can be an array of values
 107       * @param null|array|string $delimiter the text that marks the point before which you want to extract
 108       *                                 Multiple delimiters can be passed as an array of string values
 109       * @param mixed $instance The instance of the delimiter after which you want to extract the text.
 110       *                            By default, this is the first instance (1).
 111       *                            A negative value means start searching from the end of the text string.
 112       *                        Or can be an array of values
 113       * @param mixed $matchMode Determines whether the match is case-sensitive or not.
 114       *                           0 - Case-sensitive
 115       *                           1 - Case-insensitive
 116       *                        Or can be an array of values
 117       * @param mixed $matchEnd Treats the end of text as a delimiter.
 118       *                          0 - Don't match the delimiter against the end of the text.
 119       *                          1 - Match the delimiter against the end of the text.
 120       *                        Or can be an array of values
 121       * @param mixed $ifNotFound value to return if no match is found
 122       *                             The default is a #N/A Error
 123       *                          Or can be an array of values
 124       *
 125       * @return mixed|mixed[] the string extracted from text before the delimiter; or the $ifNotFound value
 126       *         If an array of values is passed for any of the arguments, then the returned result
 127       *            will also be an array with matching dimensions
 128       */
 129      public static function before($text, $delimiter, $instance = 1, $matchMode = 0, $matchEnd = 0, $ifNotFound = '#N/A')
 130      {
 131          if (is_array($text) || is_array($instance) || is_array($matchMode) || is_array($matchEnd) || is_array($ifNotFound)) {
 132              return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound);
 133          }
 134  
 135          $text = Helpers::extractString($text ?? '');
 136          $instance = (int) $instance;
 137          $matchMode = (int) $matchMode;
 138          $matchEnd = (int) $matchEnd;
 139  
 140          $split = self::validateTextBeforeAfter($text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound);
 141          if (is_array($split) === false) {
 142              return $split;
 143          }
 144          if (Helpers::extractString(Functions::flattenSingleValue($delimiter ?? '')) === '') {
 145              return ($instance > 0) ? '' : $text;
 146          }
 147  
 148          // Adjustment for a match as the first element of the split
 149          $flags = self::matchFlags($matchMode);
 150          $delimiter = self::buildDelimiter($delimiter);
 151          $adjust = preg_match('/^' . $delimiter . "\$/{$flags}", $split[0]);
 152          $oddReverseAdjustment = count($split) % 2;
 153  
 154          $split = ($instance < 0)
 155              ? array_slice($split, 0, max(count($split) - (abs($instance) * 2 - 1) - $adjust - $oddReverseAdjustment, 0))
 156              : array_slice($split, 0, $instance * 2 - 1 - $adjust);
 157  
 158          return implode('', $split);
 159      }
 160  
 161      /**
 162       * TEXTAFTER.
 163       *
 164       * @param mixed $text the text that you're searching
 165       * @param null|array|string $delimiter the text that marks the point before which you want to extract
 166       *                                 Multiple delimiters can be passed as an array of string values
 167       * @param mixed $instance The instance of the delimiter after which you want to extract the text.
 168       *                          By default, this is the first instance (1).
 169       *                          A negative value means start searching from the end of the text string.
 170       *                        Or can be an array of values
 171       * @param mixed $matchMode Determines whether the match is case-sensitive or not.
 172       *                            0 - Case-sensitive
 173       *                            1 - Case-insensitive
 174       *                         Or can be an array of values
 175       * @param mixed $matchEnd Treats the end of text as a delimiter.
 176       *                          0 - Don't match the delimiter against the end of the text.
 177       *                          1 - Match the delimiter against the end of the text.
 178       *                        Or can be an array of values
 179       * @param mixed $ifNotFound value to return if no match is found
 180       *                             The default is a #N/A Error
 181       *                          Or can be an array of values
 182       *
 183       * @return mixed|mixed[] the string extracted from text before the delimiter; or the $ifNotFound value
 184       *         If an array of values is passed for any of the arguments, then the returned result
 185       *            will also be an array with matching dimensions
 186       */
 187      public static function after($text, $delimiter, $instance = 1, $matchMode = 0, $matchEnd = 0, $ifNotFound = '#N/A')
 188      {
 189          if (is_array($text) || is_array($instance) || is_array($matchMode) || is_array($matchEnd) || is_array($ifNotFound)) {
 190              return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound);
 191          }
 192  
 193          $text = Helpers::extractString($text ?? '');
 194          $instance = (int) $instance;
 195          $matchMode = (int) $matchMode;
 196          $matchEnd = (int) $matchEnd;
 197  
 198          $split = self::validateTextBeforeAfter($text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound);
 199          if (is_array($split) === false) {
 200              return $split;
 201          }
 202          if (Helpers::extractString(Functions::flattenSingleValue($delimiter ?? '')) === '') {
 203              return ($instance < 0) ? '' : $text;
 204          }
 205  
 206          // Adjustment for a match as the first element of the split
 207          $flags = self::matchFlags($matchMode);
 208          $delimiter = self::buildDelimiter($delimiter);
 209          $adjust = preg_match('/^' . $delimiter . "\$/{$flags}", $split[0]);
 210          $oddReverseAdjustment = count($split) % 2;
 211  
 212          $split = ($instance < 0)
 213              ? array_slice($split, count($split) - (abs($instance + 1) * 2) - $adjust - $oddReverseAdjustment)
 214              : array_slice($split, $instance * 2 - $adjust);
 215  
 216          return implode('', $split);
 217      }
 218  
 219      /**
 220       * @param null|array|string $delimiter
 221       * @param int $matchMode
 222       * @param int $matchEnd
 223       * @param mixed $ifNotFound
 224       *
 225       * @return string|string[]
 226       */
 227      private static function validateTextBeforeAfter(string $text, $delimiter, int $instance, $matchMode, $matchEnd, $ifNotFound)
 228      {
 229          $flags = self::matchFlags($matchMode);
 230          $delimiter = self::buildDelimiter($delimiter);
 231  
 232          if (preg_match('/' . $delimiter . "/{$flags}", $text) === 0 && $matchEnd === 0) {
 233              return $ifNotFound;
 234          }
 235  
 236          $split = preg_split('/' . $delimiter . "/{$flags}", $text, 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
 237          if ($split === false) {
 238              return ExcelError::NA();
 239          }
 240  
 241          if ($instance === 0 || abs($instance) > StringHelper::countCharacters($text)) {
 242              return ExcelError::VALUE();
 243          }
 244  
 245          if ($matchEnd === 0 && (abs($instance) > floor(count($split) / 2))) {
 246              return ExcelError::NA();
 247          } elseif ($matchEnd !== 0 && (abs($instance) - 1 > ceil(count($split) / 2))) {
 248              return ExcelError::NA();
 249          }
 250  
 251          return $split;
 252      }
 253  
 254      /**
 255       * @param null|array|string $delimiter the text that marks the point before which you want to extract
 256       *                                 Multiple delimiters can be passed as an array of string values
 257       */
 258      private static function buildDelimiter($delimiter): string
 259      {
 260          if (is_array($delimiter)) {
 261              $delimiter = Functions::flattenArray($delimiter);
 262              $quotedDelimiters = array_map(
 263                  function ($delimiter) {
 264                      return preg_quote($delimiter ?? '');
 265                  },
 266                  $delimiter
 267              );
 268              $delimiters = implode('|', $quotedDelimiters);
 269  
 270              return '(' . $delimiters . ')';
 271          }
 272  
 273          return '(' . preg_quote($delimiter ?? '') . ')';
 274      }
 275  
 276      private static function matchFlags(int $matchMode): string
 277      {
 278          return ($matchMode === 0) ? 'mu' : 'miu';
 279      }
 280  }