<?php

// ================================================================
// Copyright (c) Meta Platforms, Inc. and affiliates.
// ================================================================

// ================================================================
// This file is bottom-up: more detailed methods at the top, main
// entry points at the bottom. Namely:
//
// * computeHashAndQualityFromFilename:
//   Returns an array of PDQHash object and integer 0-100 quality.
//
// * computeHashesAndQualityFromFilename:
//   Returns an array of [array of eight PDQHash objects keyed by rotation/flip
//   name] and integer 0-100 quality.
//
// These first two are pure-PHP. The downsample phase is the most expensive
// part of PDQ, so these use the GD library to downsample first.
//
// * computeStringHashAndQualityFromFilenameUsingExtension:
//   Returns an array of hex-string hash and integer 0-100 quality.
//
// * computeStringHashesAndQualityFromFilenameUsingExtension
//   Returns array of [array of eight hex-string hashes keyed by rotation/flip
//   name] and integer 0-100 quality.
//
// These last two use a C-language Zend extension to do most of the work.
// ================================================================

require 'pdqhash.php';

class PDQHasher {
    const LUMA_FROM_R_COEFF = 0.299;
    const LUMA_FROM_G_COEFF = 0.587;
    const LUMA_FROM_B_COEFF = 0.114;

    const PDQ_JAROSZ_WINDOW_SIZE_DIVISOR = 128;
    const PDQ_NUM_JAROSZ_XY_PASSES = 2;

    // Hashes for various dihedral transformations of the image.
    // Note, you can also transform the image and hash that.
    const DIH_ORIGINAL     = 0x01;
    const DIH_ROTATE_90    = 0x02;
    const DIH_ROTATE_180   = 0x04;
    const DIH_ROTATE_270   = 0x08;
    const DIH_FLIP_X       = 0x10;
    const DIH_FLIP_Y       = 0x20;
    const DIH_FLIP_PLUS_1  = 0x40;
    const DIH_FLIP_MINUS_1 = 0x80;
    const DIH_ALL          = 0xff;

  // ----------------------------------------------------------------
  // Handles greyscale or RGB.
  //
  // It's not obvious (to me) which is which: for JPEG the channel count is
  // available from getimagesize() while for PNG it isn't.
  //
  // But it doesn't matter due to how PHP handles pixels: for RGB the triples
  // are OR'ed into a 24-bit value while for greyscale they're the lower 8
  // bits. So we get greyscale as 'blue', multiplied by an arbitrary scaling
  // coefficient which doesn't affect the median property of the DCT output.

  static function imageToLumaMatrix(
    $image, // resource
    $num_rows,
    $num_cols
  ) {
    $luma_matrix = array();
    for ($i = 0; $i < $num_rows; $i++) {
      $row = array();
      for ($j = 0; $j < $num_cols; $j++) {
        $pixel = imagecolorat($image, $j, $i);
        $r = $pixel >> 16;
        $g = ($pixel >> 8) & 0xff;
        $b = $pixel & 0xff;
        $y = self::LUMA_FROM_R_COEFF * $r
           + self::LUMA_FROM_G_COEFF * $g
           + self::LUMA_FROM_B_COEFF * $b;
        $row[$j] = $y;
      }
      $luma_matrix[$i] = $row;
    }
    return $luma_matrix;
  }

  // ================================================================
  // Wojciech Jarosz 'Fast Image Convolutions' ACM SIGGRAPH 2001:
  // X,Y,X,Y passes of 1-D box filters produces a 2D tent filter.
  //
  // Since PDQ uses 64x64 blocks, 1/64th of the image height/width respectively is
  // a full block. But since we use two passes, we want half that window size per
  // pass. Example: 1024x1024 full-resolution input. PDQ downsamples to 64x64.
  // Each 16x16 block of the input produces a single downsample pixel.  X,Y passes
  // with window size 8 (= 1024/128) average pixels with 8x8 neighbors. The second
  // X,Y pair of 1D box-filter passes accumulate data from all 16x16.

  // ----------------------------------------------------------------
  static function computeJaroszFilterWindowSize(
    $dimension
  ) {

    return (int)(($dimension + self::PDQ_JAROSZ_WINDOW_SIZE_DIVISOR - 1)
      / self::PDQ_JAROSZ_WINDOW_SIZE_DIVISOR);
  }

  // ----------------------------------------------------------------
  // 7 and 4
  //
  //    0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1
  //    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
  //
  //    .                                PHASE 1: ONLY ADD, NO WRITE, NO SUBTRACT
  //    . .
  //    . . .
  //
  //  0 * . . .                          PHASE 2: ADD, WRITE, WITH NO SUBTRACTS
  //  1 . * . . .
  //  2 . . * . . .
  //  3 . . . * . . .
  //
  //  4   . . . * . . .                  PHASE 3: WRITES WITH ADD & SUBTRACT
  //  5     . . . * . . .
  //  6       . . . * . . .
  //  7         . . . * . . .
  //  8           . . . * . . .
  //  9             . . . * . . .
  // 10               . . . * . . .
  // 11                 . . . * . . .
  // 12                   . . . * . . .
  //
  // 13                     . . . * . .  PHASE 4: FINAL WRITES WITH NO ADDS
  // 14                       . . . * .
  // 15                         . . . *
  //
  //         = 0                                     =  0   PHASE 1
  //         = 0+1                                   =  1
  //         = 0+1+2                                 =  3
  //
  // out[ 0] = 0+1+2+3                               =  6   PHASE 2
  // out[ 1] = 0+1+2+3+4                             = 10
  // out[ 2] = 0+1+2+3+4+5                           = 15
  // out[ 3] = 0+1+2+3+4+5+6                         = 21
  //
  // out[ 4] =   1+2+3+4+5+6+7                       = 28   PHASE 3
  // out[ 5] =     2+3+4+5+6+7+8                     = 35
  // out[ 6] =       3+4+5+6+7+8+9                   = 42
  // out[ 7] =         4+5+6+7+8+9+10                = 49
  // out[ 8] =           5+6+7+8+9+10+11             = 56
  // out[ 9] =             6+7+8+9+10+11+12          = 63
  // out[10] =               7+8+9+10+11+12+13       = 70
  // out[11] =                 8+9+10+11+12+13+14    = 77
  // out[12] =                   9+10+11+12+13+14+15 = 84
  //
  // out[13] =                     10+11+12+13+14+15 = 75  PHASE 4
  // out[14] =                        11+12+13+14+15 = 65
  // out[15] =                           12+13+14+15 = 54
  // ----------------------------------------------------------------

  // ----------------------------------------------------------------
  // 8 and 5
  //
  //    0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1
  //    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
  //
  //    .                                PHASE 1: ONLY ADD, NO WRITE, NO SUBTRACT
  //    . .
  //    . . .
  //    . . . .
  //
  //  0 * . . . .                        PHASE 2: ADD, WRITE, WITH NO SUBTRACTS
  //  1 . * . . . .
  //  2 . . * . . . .
  //  3 . . . * . . . .
  //
  //  4   . . . * . . . .                PHASE 3: WRITES WITH ADD & SUBTRACT
  //  5     . . . * . . . .
  //  6       . . . * . . . .
  //  7         . . . * . . . .
  //  8           . . . * . . . .
  //  9             . . . * . . . .
  // 10               . . . * . . . .
  // 11                 . . . * . . . .
  //
  // 12                   . . . * . . .  PHASE 4: FINAL WRITES WITH NO ADDS
  // 13                     . . . * . .
  // 14                       . . . * .
  // 15                         . . . *
  //
  //         = 0                                     =  0   PHASE 1
  //         = 0+1                                   =  1
  //         = 0+1+2                                 =  3
  //         = 0+1+2+3                               =  6
  //
  // out[ 0] = 0+1+2+3+4                             = 10
  // out[ 1] = 0+1+2+3+4+5                           = 15
  // out[ 2] = 0+1+2+3+4+5+6                         = 21
  // out[ 3] = 0+1+2+3+4+5+6+7                       = 28
  //
  // out[ 4] =   1+2+3+4+5+6+7+8                     = 36   PHASE 3
  // out[ 5] =     2+3+4+5+6+7+8+9                   = 44
  // out[ 6] =       3+4+5+6+7+8+9+10                = 52
  // out[ 7] =         4+5+6+7+8+9+10+11             = 60
  // out[ 8] =           5+6+7+8+9+10+11+12          = 68
  // out[ 9] =             6+7+8+9+10+11+12+13       = 76
  // out[10] =               7+8+9+10+11+12+13+14    = 84
  // out[11] =                 8+9+10+11+12+13+14+15 = 92
  //
  // out[12] =                   9+10+11+12+13+14+15 = 84  PHASE 4
  // out[13] =                     10+11+12+13+14+15 = 75  PHASE 4
  // out[14] =                        11+12+13+14+15 = 65
  // out[15] =                           12+13+14+15 = 54
  // ----------------------------------------------------------------

  // ----------------------------------------------------------------
  static function boxAlongCols(
    &$in_image, // 2D array of float
    &$out_image, // 2D array of float
    $num_rows,
    $num_cols,
    $window_size
  ) {
    for ($j = 0; $j < $num_cols; $j++) {
      $half_window_size = (int)(($window_size + 2) / 2); // 7->4, 8->5

      $phase_1_nreps = $half_window_size - 1;
      $phase_2_nreps = $window_size - $half_window_size + 1;
      $phase_3_nreps = $num_rows - $window_size;
      $phase_4_nreps = $half_window_size - 1;

      $li = 0; // Index of left edge of read window, for subtracts
      $ri = 0; // Index of right edge of read windows, for adds
      $oi = 0; // Index into output vector

      $sum = 0.0;
      $current_window_size = 0;

      // PHASE 1: ACCUMULATE FIRST SUM NO WRITES
      for ($k = 0; $k < $phase_1_nreps; $k++) {
        $sum += $in_image[$ri][$j];
        $current_window_size++;
        $ri++;
      }

      // PHASE 2: INITIAL WRITES WITH SMALL WINDOW
      for ($k = 0; $k < $phase_2_nreps; $k++) {
        $sum += $in_image[$ri][$j];
        $current_window_size++;
        $out_image[$oi][$j] = $sum / $current_window_size;
        $ri++;
        $oi++;
      }

      // PHASE 3: WRITES WITH FULL WINDOW
      for ($k = 0; $k < $phase_3_nreps; $k++) {
        $sum += $in_image[$ri][$j];
        $sum -= $in_image[$li][$j];
        $out_image[$oi][$j] = $sum / $current_window_size;
        $li++;
        $ri++;
        $oi++;
      }

      // PHASE 4: FINAL WRITES WITH SMALL WINDOW
      for ($k = 0; $k < $phase_4_nreps; $k++) {
        $sum -= $in_image[$li][$j];
        $current_window_size--;
        $out_image[$oi][$j] = $sum / $current_window_size;
        $li++;
        $oi++;
      }
    }
  }

  static function boxAlongRows(
    &$in_image, // 2D array of float
    &$out_image, // 2D array of float
    $num_rows,
    $num_cols,
    $window_size
  ) {
    for ($i = 0; $i < $num_rows; $i++) {
      $half_window_size = (int)(($window_size + 2) / 2); // 7->4, 8->5

      $phase_1_nreps = $half_window_size - 1;
      $phase_2_nreps = $window_size - $half_window_size + 1;
      $phase_3_nreps = $num_cols - $window_size;
      $phase_4_nreps = $half_window_size - 1;

      $li = 0; // Index of left edge of read window, for subtracts
      $ri = 0; // Index of right edge of read windows, for adds
      $oi = 0; // Index into output vector

      $sum = 0.0;
      $current_window_size = 0;

      // PHASE 1: ACCUMULATE FIRST SUM NO WRITES
      for ($k = 0; $k < $phase_1_nreps; $k++) {
        $sum += $in_image[$i][$ri];
        $current_window_size++;
        $ri++;
      }

      // PHASE 2: INITIAL WRITES WITH SMALL WINDOW
      for ($k = 0; $k < $phase_2_nreps; $k++) {
        $sum += $in_image[$i][$ri];
        $current_window_size++;
        $out_image[$i][$oi] = $sum / $current_window_size;
        $ri++;
        $oi++;
      }

      // PHASE 3: WRITES WITH FULL WINDOW
      for ($k = 0; $k < $phase_3_nreps; $k++) {
        $sum += $in_image[$i][$ri];
        $sum -= $in_image[$i][$li];
        $out_image[$i][$oi] = $sum / $current_window_size;
        $li++;
        $ri++;
        $oi++;
      }

      // PHASE 4: FINAL WRITES WITH SMALL WINDOW
      for ($k = 0; $k < $phase_4_nreps; $k++) {
        $sum -= $in_image[$i][$li];
        $current_window_size--;
        $out_image[$i][$oi] = $sum / $current_window_size;
        $li++;
        $oi++;
      }
    }
  }

  // ----------------------------------------------------------------
  static function jaroszFilter(
    &$luma_matrix, // 2D array of float
    $num_rows,
    $num_cols,
    $window_size_along_rows,
    $window_size_along_cols
  ) {

    $other_matrix = array();
    for ($i = 0; $i < $num_rows; $i++) {
      $row = array();
      for ($j = 0; $j < $num_cols; $j++) {
        $row[$j] = 0;
      }
      $other_matrix[$i] = $row;
    }

    for ($k = 0; $k < self::PDQ_NUM_JAROSZ_XY_PASSES; $k++) {
      self::boxAlongRows($luma_matrix, $other_matrix, $num_rows, $num_cols, $window_size_along_rows);
      self::boxAlongCols($other_matrix, $luma_matrix, $num_rows, $num_cols, $window_size_along_cols);
    }

  }

  // ================================================================
  // This is all heuristic (see the PDQ hashing doc). Quantization matters since
  // we want to count *significant* gradients, not just the some of many small
  // ones. The constants are all manually selected, and tuned as described in the
  // document.
  static function computeImageDomainQualityMetric(
    &$buffer_64x64
  ) {
    $int_gradient_sum = 0;

    for ($i = 0; $i < 63; $i++) {
      for ($j = 0; $j < 64; $j++) {
        $u = $buffer_64x64[$i][$j];
        $v = $buffer_64x64[$i+1][$j];
        $d = (int)((($u - $v) * 100) / 255);
        $int_gradient_sum += (int)abs($d);
      }
    }
    for ($i = 0; $i < 64; $i++) {
      for ($j = 0; $j < 63; $j++) {
        $u = $buffer_64x64[$i][$j];
        $v = $buffer_64x64[$i][$j+1];
        $$d = (int)((($u - $v) * 100) / 255);
        $int_gradient_sum += (int)abs($d);
      }
    }

    // Heuristic scaling factor.
    $quality = (int)($int_gradient_sum / 90);
    if ($quality > 100) {
      $quality = 100;
    }

    return $quality;
  }

  // ================================================================
  // Full 64x64 to 64x64 can be optimized e.g. the Lee algorithm.  But here we
  // only want slots (1-16)x(1-16) of the full 64x64 output. Careful experiments
  // showed that using Lee along all 64 slots in one dimension, then Lee along 16
  // slots in the second, followed by extracting slots 1-16 of the output, was
  // actually slower than the current implementation which is completely
  // non-clever/non-Lee but computes only what is needed.

  static function computeDCT64To16(
    &$buffer_64x64,
    &$buffer_16x64,
    &$buffer_16x16,
    &$dct_16x64
  ) {
    // A = buffer_64x64
    // T = buffer_16x64
    // B = buffer_16x16
    // D = DCT matrix

    // 2D DCT:
    //   B = D A Dt
    // Split out into first product and second:
    //   B = (D A) Dt ; T = D A

    for ($i = 0; $i < 16; $i++) {
      for ($j = 0; $j < 64; $j++) {
        $sumk = 0.0;
        for ($k = 0; $k < 64; $k++) {
          $sumk += $dct_16x64[$i][$k] * $buffer_64x64[$k][$j];
        }
        $buffer_16x64[$i][$j] = $sumk;
      }
    }

    for ($i = 0; $i < 16; $i++) {
      for ($j = 0; $j < 16; $j++) {
        $sumk = 0.0;
          // sumk += T[i][k] * D[j][k];
        for ($k = 0; $k < 64; $k++) {
          $sumk += $buffer_16x64[$i][$k] * $dct_16x64[$j][$k];
        }
        $buffer_16x16[$i][$j] = $sumk;
      }
    }
  }

  // ----------------------------------------------------------------
  // orig      rot90     rot180    rot270
  // noxpose   xpose     noxpose   xpose
  // + + + +   - + - +   + - + -   - - - -
  // + + + +   - + - +   - + - +   + + + +
  // + + + +   - + - +   + - + -   - - - -
  // + + + +   - + - +   - + - +   + + + +
  //
  // flipx     flipy     flipplus  flipminus
  // noxpose   noxpose   xpose     xpose
  // - - - -   - + - +   + + + +   + - + -
  // + + + +   - + - +   + + + +   - + - +
  // - - - -   - + - +   + + + +   + - + -
  // + + + +   - + - +   + + + +   - + - +

  static function dct16OriginalToRotate90(&$A, &$B) {
    for ($i = 0; $i < 16; $i++) {
      for ($j = 0; $j < 16; $j++) {
        if ($j & 1) {
          $B[$j][$i] = $A[$i][$j];
        } else {
          $B[$j][$i] = -$A[$i][$j];
        }
      }
    }
  }

  static function dct16OriginalToRotate180(&$A, &$B) {
    for ($i = 0; $i < 16; $i++) {
      for ($j = 0; $j < 16; $j++) {
        if (($i+$j) & 1) {
          $B[$i][$j] = -$A[$i][$j];
        } else {
          $B[$i][$j] = $A[$i][$j];
        }
      }
    }
  }

  static function dct16OriginalToRotate270(&$A, &$B) {
    for ($i = 0; $i < 16; $i++) {
      for ($j = 0; $j < 16; $j++) {
        if ($i & 1) {
          $B[$j][$i] = $A[$i][$j];
        } else {
          $B[$j][$i] = -$A[$i][$j];
        }
      }
    }
  }

  static function dct16OriginalToFlipX(&$A, &$B) {
    for ($i = 0; $i < 16; $i++) {
      for ($j = 0; $j < 16; $j++) {
        if ($i & 1) {
          $B[$i][$j] = $A[$i][$j];
        } else {
          $B[$i][$j] = -$A[$i][$j];
        }
      }
    }
  }

  static function dct16OriginalToFlipY(&$A, &$B) {
    for ($i = 0; $i < 16; $i++) {
      for ($j = 0; $j < 16; $j++) {
        if ($j & 1) {
          $B[$i][$j] = $A[$i][$j];
        } else {
          $B[$i][$j] = -$A[$i][$j];
        }
      }
    }
  }

  static function dct16OriginalToFlipPlus1(&$A, &$B) {
    for ($i = 0; $i < 16; $i++) {
      for ($j = 0; $j < 16; $j++) {
        $B[$j][$i] = $A[$i][$j];
      }
    }
  }

  static function dct16OriginalToFlipMinus1(&$A, &$B) {
    for ($i = 0; $i < 16; $i++) {
      for ($j = 0; $j < 16; $j++) {
        if (($i+$j) & 1) {
          $B[$j][$i] = -$A[$i][$j];
        } else {
          $B[$j][$i] = $A[$i][$j];
        }
      }
    }
  }

  // ----------------------------------------------------------------
  static function computeHashFromDCTOutput(
    &$buffer_16x16
  ) {
    $flat_matrix = array();
    for ($k = 0, $i = 0; $i < 16; $i++) {
      for ($j = 0; $j < 16; $j++, $k++) {
        $flat_matrix[$k] = $buffer_16x16[$i][$j];
      }
    }

    sort($flat_matrix);
    //print_r($flat_matrix);

    $median = $flat_matrix[127];
    //echo "median=$median\n";

    $hash = PDQHash::makeZeroesHash();
    for ($k = 0, $i = 0; $i < 16; $i++) {
      for ($j = 0; $j < 16; $j++, $k++) {
        $value = $buffer_16x16[$i][$j];
        if ($value > $median) {
          $hash->setBit($k);
        }
      }
    }

    return $hash;
  }

  // ================================================================
  static function readImageFromFilename($filename, $downsample_first) {
    $is_jpeg = false;
    if (substr_compare($filename,'.jpg', -strlen('.jpg')) === 0) {
      $orig_image = imagecreatefromjpeg($filename);
      $is_jpeg = true;
    } else if (substr_compare($filename,'.jpeg', -strlen('.jpeg')) === 0) {
      $orig_image = imagecreatefromjpeg($filename);
      $is_jpeg = true;
    } else if (substr_compare($filename,'.png', -strlen('.png')) === 0) {
      $orig_image = imagecreatefrompng($filename);
    } else {
      throw new Exception('PDQHasher: could not handle filetype of '.$filename);
    }

    // The pure-PHP hasher is *really* slow in pure PHP for megapixel images.
    // So, downsample first. Don't worry about aspect ratio since PDQ will
    // squarify anyway. For extension use, don't downsample here as it's
    // redundant.
    if ($downsample_first) {
      $orig_height = imagesy($orig_image);
      $orig_width = imagesx($orig_image);
      if ($orig_height > 128 || $orig_width > 128) {
        $image = imagecreatetruecolor(128, 128);
        imagecopyresampled($image, $orig_image, 0, 0, 0, 0, 128, 128, $orig_width, $orig_height);
      } else {
        $image = $orig_image;
      }
    } else {
      $image = $orig_image;
    }

// NOTE: the PDQ hashes within ThreatExchange aren't respecting EXIF rotation tags
// so we should likewise ignore them.
//
//    if ($is_jpeg) {
//      $exif = exif_read_data($filename);
//      if (!empty($exif['Orientation'])) {
//        switch ($exif['Orientation']) {
//        case 3:
//          $image = imagerotate($image, 180, 0);
//          break;
//        case 6:
//          $image = imagerotate($image, -90, 0);
//          break;
//        case 8:
//          $image = imagerotate($image, 90, 0);
//          break;
//        }
//      }
//    }

    return $image;
  }

  // ================================================================
  static function computeDCTAndQualityFromImage(
    /*resource*/&$image,
    /*bool*/ $show_timings,
    /*bool*/ $dump
  ) {
    $num_rows = imagesy($image);
    $num_cols = imagesx($image);
    if ($dump) {
      echo "num_rows=$num_rows\n";
      echo "num_cols=$num_cols\n";
    }

    //  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // RGB to luma
    $t1 = microtime(true);
    $luma_matrix = self::imageToLumaMatrix($image, $num_rows, $num_cols);
    $t2 = microtime(true);
    if ($show_timings) {
      printf("X010-LUMA %.6f\n", $t2-$t1);
    }

    //  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Downsample (blur and decimate)
    $t1 = microtime(true);
    $window_size_along_rows = self::computeJaroszFilterWindowSize($num_cols);
    $window_size_along_cols = self::computeJaroszFilterWindowSize($num_rows);
    self::jaroszFilter($luma_matrix, $num_rows, $num_cols, $window_size_along_rows, $window_size_along_cols);
    $t2 = microtime(true);
    if ($show_timings) {
      printf("X020-JRSZ %.6f\n", $t2-$t1);
    }

    // Decimation per se. Target centers not corners.
    $buffer_64x64 = array();
    for ($i = 0; $i < 64; $i++) {
      $row = array();
      for ($j = 0; $j < 64; $j++) {
        $row[$j] = 0;
      }
      $buffer_64x64[$i] = $row;
    }
    $t1 = microtime(true);
    for ($i = 0; $i < 64; $i++) {
      $ini = (int)((($i + 0.5) * $num_rows) / 64);
      for ($j = 0; $j < 64; $j++) {
        $inj = (int)((($j + 0.5) * $num_cols) / 64);
        $buffer_64x64[$i][$j] = $luma_matrix[$ini][$inj];
      }
    }
    $t2 = microtime(true);
    if ($dump) {
      echo "DOWNSAMPLE IMAGE:\n";
      print_r($buffer_64x64);
    }
    if ($show_timings) {
      printf("X030-DSMP %.6f\n", $t2-$t1);
    }

    //  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Quality metric.  Reuse the 64x64 image-domain downsample
    // since we already have it.
    $t1 = microtime(true);
    $quality = self::computeImageDomainQualityMetric($buffer_64x64);
    $t2 = microtime(true);
    if ($show_timings) {
      printf("X040-QMTC %.6f\n", $t2-$t1);
    }
    if ($dump) {
      echo "QUALITY:$quality\n";
    }

    //  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    $t1 = microtime(true);

    $buffer_16x64 = array();
    for ($i = 0; $i < 16; $i++) {
      $row = array();
      for ($j = 0; $j < 64; $j++) {
        $row[$j] = 0;
      }
      $buffer_16x64[$i] = $row;
    }

    $buffer_16x16 = array();
    for ($i = 0; $i < 16; $i++) {
      $row = array();
      for ($j = 0; $j < 16; $j++) {
        $row[$j] = 0;
      }
      $buffer_16x16[$i] = $row;
    }

    $dct_16x64 = array();
    for ($i = 0; $i < 16; $i++) {
      $row = array();
      for ($j = 0; $j < 64; $j++) {
        $row[$j] = 0;
      }
      $dct_16x64[$i] = $row;
    }
    // See comments on dct64To16. Input is (0..63)x(0..63); output is
    // (1..16)x(1..16) with the latter indexed as (0..15)x(0..15).
    $matrix_scale_factor = sqrt(2.0 / 64.0);
    $pi = 3.141592653589793;
    for ($i = 0; $i < 16; $i++) {
      for ($j = 0; $j < 64; $j++) {
        $dct_16x64[$i][$j] = $matrix_scale_factor *
          cos(($pi / 2 / 64.0) * ($i+1) * (2 * $j + 1));
      }
    }
    $t2 = microtime(true);
    if ($show_timings) {
      printf("X050-DMTX %.6f\n", $t2-$t1);
    }
    if ($dump) {
      echo "DCT MATRIX:\n";
      print_r($dct_16x64);
    }

    // 2D DCT
    $t1 = microtime(true);
    self::computeDCT64To16($buffer_64x64, $buffer_16x64, $buffer_16x16, $dct_16x64);
    $t2 = microtime(true);
    if ($show_timings) {
      printf("X060-CDCT %.6f\n", $t2-$t1);
    }
    if ($dump) {
      echo "DCT OUTPUT:\n";
      print_r($buffer_16x16);
    }

    return array($buffer_16x16, $quality);
  }

  // ----------------------------------------------------------------
  static function computeHashAndQualityFromImage(
    /*resource*/&$image,
    /*bool*/ $show_timings,
    /*bool*/ $dump
  ) {
    $t01 = microtime(true);

    //  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    list ($buffer_16x16, $quality) = self:: computeDCTAndQualityFromImage(
      $image, $show_timings, $dump
    );

    //  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    $hash = self::computeHashFromDCTOutput($buffer_16x16);
    if ($dump) {
      echo "HASH:".$hash->toHexString()."\n";
    }
    $t02 = microtime(true);
    if ($show_timings) {
      printf("X999-OVRL %.6f\n", $t02-$t01);
    }

    //  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    return array($hash, $quality);
  }

  // ----------------------------------------------------------------
  static function computeHashesAndQualityFromImage(
    /*resource*/&$image,
    /*int*/ $which_flags = self::DIH_ALL,
    /*bool*/ $show_timings,
    /*bool*/ $dump
  ) {
    $t01 = microtime(true);

    //  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    list ($buffer_16x16, $quality) = self:: computeDCTAndQualityFromImage(
      $image, $show_timings, $dump
    );

    $buffer_16x16_aux = array();
    for ($i = 0; $i < 16; $i++) {
      $row = array();
      for ($j = 0; $j < 16; $j++) {
        $row[$j] = 0;
      }
      $buffer_16x16_aux[$i] = $row;
    }

    //  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    $hashes = array();

    if ($which_flags & self::DIH_ORIGINAL) {
      $hashes['orig'] = self::computeHashFromDCTOutput($buffer_16x16);
    }

    if ($which_flags & self::DIH_ROTATE_90) {
      self::dct16OriginalToRotate90($buffer_16x16, $buffer_16x16Aux);
      $hashes['r090'] = self::computeHashFromDCTOutput($buffer_16x16Aux);
    }

    if ($which_flags & self::DIH_ROTATE_180) {
      self::dct16OriginalToRotate180($buffer_16x16, $buffer_16x16Aux);
      $hashes['r180'] = self::computeHashFromDCTOutput($buffer_16x16Aux);
    }

    if ($which_flags & self::DIH_ROTATE_270) {
      self::dct16OriginalToRotate270($buffer_16x16, $buffer_16x16Aux);
      $hashes['r270'] = self::computeHashFromDCTOutput($buffer_16x16Aux);
    }

    if ($which_flags & self::DIH_FLIP_X) {
      self::dct16OriginalToFlipX($buffer_16x16, $buffer_16x16Aux);
      $hashes['flpx'] = self::computeHashFromDCTOutput($buffer_16x16Aux);
    }

    if ($which_flags & self::DIH_FLIP_Y) {
      self::dct16OriginalToFlipY($buffer_16x16, $buffer_16x16Aux);
      $hashes['flpy'] = self::computeHashFromDCTOutput($buffer_16x16Aux);
    }

    if ($which_flags & self::DIH_FLIP_PLUS_1) {
      self::dct16OriginalToFlipPlus1($buffer_16x16, $buffer_16x16Aux);
      $hashes['flpp'] = self::computeHashFromDCTOutput($buffer_16x16Aux);
    }

    if ($which_flags & self::DIH_FLIP_MINUS_1) {
      self::dct16OriginalToFlipMinus1($buffer_16x16, $buffer_16x16Aux);
      $hashes['flpm'] = self::computeHashFromDCTOutput($buffer_16x16Aux);
    }

    $t02 = microtime(true);
    if ($show_timings) {
      printf("X999-OVRL %.6f\n", $t02-$t01);
    }

    //  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    return array($hashes, $quality);
  }

  // ================================================================
  static function computeHashAndQualityFromFilename(
    $filename,
    $show_timings = false,
    $dump = false,
    $downsample = false
  ) {

    $t01 = microtime(true);

    //  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    $info = getimagesize($filename);
    if ($dump) {
      echo "IMAGE INFO:\n";
      print_r($info);
    }

    $num_rows = $info[1]; // height
    $num_cols = $info[0]; // width
    if ($dump) {
      echo "num_rows=$num_rows\n";
      echo "num_cols=$num_cols\n";
    }

    //  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    $t1 = microtime(true);
    $image = self::readImageFromFilename($filename, $downsample);
    $t2 = microtime(true);
    if ($show_timings) {
      printf("X000-READ %.6f\n", $t2-$t1);
    }

    return self::computeHashAndQualityFromImage($image, $show_timings, $dump);
  }

  // ----------------------------------------------------------------
  static function computeHashesAndQualityFromFilename(
    $filename,
    $which_flags = self::DIH_ALL,
    $show_timings = false,
    $dump = false
  ) {

    $t01 = microtime(true);

    //  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    $info = getimagesize($filename);
    if ($dump) {
      echo "IMAGE INFO:\n";
      print_r($info);
    }

    $num_rows = $info[1]; // height
    $num_cols = $info[0]; // width
    if ($dump) {
      echo "num_rows=$num_rows\n";
      echo "num_cols=$num_cols\n";
    }

    //  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    $t1 = microtime(true);
    $image = self::readImageFromFilename($filename, true);
    $t2 = microtime(true);
    if ($show_timings) {
      printf("X000-READ %.6f\n", $t2-$t1);
    }

    return self::computeHashesAndQualityFromImage(
      $image, $which_flags, $show_timings, $dump
    );
  }

  // ================================================================
  // Array of hash and quality.
  // The hash is a hex-string, not a PDQHash object.

  static function computeStringHashAndQualityFromFilenameUsingExtension(
    $filename,
    $show_timings = false,
    $dump = false
  ) {

    $t01 = microtime(true);

    //  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    $info = getimagesize($filename);
    if ($dump) {
      echo "IMAGE INFO:\n";
      print_r($info);
    }

    $num_rows = $info[1]; // height
    $num_cols = $info[0]; // width
    if ($dump) {
      echo "num_rows=$num_rows\n";
      echo "num_cols=$num_cols\n";
    }

    //  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    $t1 = microtime(true);
    $image = self::readImageFromFilename($filename, false);
    $t2 = microtime(true);
    if ($show_timings) {
      printf("X000-READ %.6f\n", $t2-$t1);
    }

    // Uses the PDQ Zend-PHP extension
    $t1 = microtime(true);
    $retval = pdq_compute_string_hash_and_quality_from_image_resource($image);
    $t2 = microtime(true);
    if ($show_timings) {
      printf("X000-EXTN %.6f\n", $t2-$t1);
    }

    return array($retval['hash'], $retval['quality']);
  }

  // ----------------------------------------------------------------
  static function computeStringHashesAndQualityFromFilenameUsingExtension(
    $filename,
    $which_flags = self::DIH_ALL,
    $show_timings = false,
    $dump = false
  ) {

    $t01 = microtime(true);

    //  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    $info = getimagesize($filename);
    if ($dump) {
      echo "IMAGE INFO:\n";
      print_r($info);
    }

    $num_rows = $info[1]; // height
    $num_cols = $info[0]; // width
    if ($dump) {
      echo "num_rows=$num_rows\n";
      echo "num_cols=$num_cols\n";
    }

    //  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    $t1 = microtime(true);
    $image = self::readImageFromFilename($filename, false);
    $t2 = microtime(true);
    if ($show_timings) {
      printf("X000-READ %.6f\n", $t2-$t1);
    }

    // Uses the PDQ Zend-PHP extension
    $t1 = microtime(true);
    $retval = pdq_compute_string_hashes_and_quality_from_image_resource($image);
    $t2 = microtime(true);
    if ($show_timings) {
      printf("X000-EXTN %.6f\n", $t2-$t1);
    }

    return array(
      array(
        'orig' => $retval['orig'],
        'r090' => $retval['r090'],
        'r180' => $retval['r180'],
        'r270' => $retval['r270'],
        'flpx' => $retval['flpx'],
        'flpy' => $retval['flpy'],
        'flpp' => $retval['flpp'],
        'flpm' => $retval['flpm'],
      ),
      $retval['quality']
    );
  }

} // class PDQHasher


<?php

// ================================================================
// Copyright (c) Meta Platforms, Inc. and affiliates.
// ================================================================

require 'pdqhasher.php';

$filenames = array_slice($argv, 1);
if (empty($filenames)) {
  fprintf(STDERR, "%s: need at least one image file name.\n", $argv[0]);
  exit(1);
}

foreach ($filenames as $filename) {

  list ($hashes, $quality) = PDQHasher::computeHashesAndQualityFromFilename($filename);
  foreach ($hashes as $type => $hash) {
    $s = $hash->toHexString();
    echo "$s,$type,$quality,purephp,$filename\n";
  }

  list ($hashes, $quality) = PDQHasher::computeStringHashesAndQualityFromFilenameUsingExtension($filename);
  foreach ($hashes as $type => $hash) {
    echo "$hash,$type,$quality,extnphp,$filename\n";
  }

}


<?php
// ================================================================
// Copyright (c) Meta Platforms, Inc. and affiliates.
// ================================================================

require 'pdqhasher.php';

$filenames = array_slice($argv, 1);
if (empty($filenames)) {
  fprintf(STDERR, "%s: need at least one image file name.\n", $argv[0]);
  exit(1);
}
$show_timings = false;
//$show_timings = true;
$dump = false;

$downsample = false;

foreach ($filenames as $filename) {

  list ($hash, $quality) = PDQHasher::computeHashAndQualityFromFilename($filename, $show_timings, $dump, $downsample);
  $s = $hash->toHexString();
  echo "$s,$quality,purephp,$filename\n";
  if (function_exists('pdq_compute_string_hash_and_quality_from_image_resource')) {
    list ($hash, $quality) = PDQHasher::computeStringHashAndQualityFromFilenameUsingExtension($filename, $show_timings, $dump);
    echo "$hash,$quality,extnphp,$filename\n";
  } else {
    echo "php extension not available";
  }

}


<?php

// ================================================================
// Copyright (c) Meta Platforms, Inc. and affiliates.
// ================================================================

require 'pdqhash.php';

if (count($argv) > 1) {
  fprintf(STDERR, "%s: takes no arguments.\n", $argv[0]);
  exit(1);
}

$hash = PDQHash::makeZeroesHash();
$s = $hash->toHexString();
echo "s=$s\n";
for ($i = 0; $i < 256; $i++) {
  $hash->setBit($i);
  $s = $hash->toHexString();
  $n = $hash->hammingNorm();
  echo "s=$s n=$n\n";
}
$s = 'f8f8f0cce0f4e84d0e370a22028f67f0b36e2ed596623e1d33e6339c4e9c9b22';
echo "s=$s\n";
$hash = PDQHash::fromHexString($s);
$s = $hash->toHexString();
echo "s=$s\n";

$strings = $hash->to16BitStrings();
echo "\n";
foreach ($strings as $string) {
  echo "$string\n";
}

$strings = $hash->to32BitStrings();
echo "\n";
foreach ($strings as $string) {
  echo "$string\n";
}

$strings = $hash->to64BitStrings();
echo "\n";
foreach ($strings as $string) {
  echo "$string\n";
}


<?php

/**
 * Encrypt given text using caesar cipher.
 *
 * @param string text text to be encrypted
 * @param int shift number of shifts to be applied
 * @return string new encrypted text
 */
function encrypt(string $text, int $shift): string
{
    $encryptedText = ''; // Empty string to store encrypted text
    foreach (str_split($text) as $c) { // Going through each character
        if (ctype_alpha($c)) {
            $placeValue = ord($c) - ord(ctype_upper($c) ? 'A' : 'a'); // Getting value of character (i.e. 0-25)
            $placeValue = ($placeValue + $shift) % 26; // Applying encryption formula
            $placeValue += ord(ctype_upper($c) ? 'A' : 'a');
            $newChar = chr($placeValue); // Getting new character from new value (i.e. A-Z)
            $encryptedText .= $newChar; // Appending encrypted character
        } else {
            $encryptedText .= $c; // Appending the original character
        }
    }

    return $encryptedText; // Returning encrypted text
}

/**
 * Decrypt given text using caesar cipher.
 * @param string text text to be decrypted
 * @param int shift number of shifts to be applied
 * @return string new decrypted text
 */
function decrypt(string $text, int $shift): string
{
    $decryptedText = ''; // Empty string to store decrypted text
    foreach (str_split($text) as $c) { // Going through each character
        if (ctype_alpha($c)) {
            $placeValue = ord($c) - ord(ctype_upper($c) ? 'A' : 'a'); // Getting value of character (i.e. 0-25)
            $placeValue = ($placeValue - $shift) % 26; // Applying decryption formula
            if ($placeValue < 0) { // Handling case where remainder is negative
                $placeValue += 26;
            }
            $placeValue += ord(ctype_upper($c) ? 'A' : 'a');
            $newChar = chr($placeValue); // Getting new character from new value (i.e. A-Z)
            $decryptedText .= $newChar; // Appending decrypted character
        } else {
            $decryptedText .= $c; // Appending the original character
        }
    }

    return $decryptedText; // Returning decrypted text
}


<?php
// A mono-alphabetic cipher is a simple substitution cipher
// https://www.101computing.net/mono-alphabetic-substitution-cipher/

function monoAlphabeticCipher($key, $alphabet, $text){

    $cipherText = ''; // the cipher text (can be decrypted and encrypted)

    if ( strlen($key) != strlen($alphabet) ) { return false; } // check if the text length matches
    $text = preg_replace('/[0-9]+/', '', $text); // remove all the numbers

    for( $i = 0; $i < strlen($text); $i++ ){
        $index = strripos( $alphabet, $text[$i] );
        if( $text[$i] == " " ){ $cipherText .= " "; }
        else{ $cipherText .= ( ctype_upper($text[$i]) ? strtoupper($key[$index]) : $key[$index] ); }
    }
    return $cipherText;
}

function maEncrypt($key, $alphabet, $text){

    return monoAlphabeticCipher($key, $alphabet, $text);

}

function maDecrypt($key, $alphabet, $text){

    return monoAlphabeticCipher($alphabet, $key, $text);

}

?>


<?php

/**
 * Encode text to Morse Code.
 *
 * @param string text to encode
 * @return string encoded text
 */
function encode(string $text): string
{
    $text = strtoupper($text); // Makes sure the string is uppercase
    $MORSE_CODE = array( // Array containing morse code translations
        "A" => ".-",
        "B" => "-...",
        "C" => "-.-.",
        "D" => "-..",
        "E" => ".",
        "F" => "..-.",
        "G" => "--.",
        "H" => "....",
        "I" => "..",
        "J" => ".---",
        "K" => "-.-",
        "L" => ".-..",
        "M" => "--",
        "N" => "-.",
        "O" => "---",
        "P" => ".--.",
        "Q" => "--.-",
        "R" => ".-.",
        "S" => "...",
        "T" => "-",
        "U" => "..-",
        "V" => "...-",
        "W" => ".--",
        "X" => "-..-",
        "Y" => "-.--",
        "Z" => "--..",
        "1" => ".----",
        "2" => "..---",
        "3" => "...--",
        "4" => "....-",
        "5" => ".....",
        "6" => "-....",
        "7" => "--...",
        "8" => "---..",
        "9" => "----.",
        "0" => "-----",
        " " => "/"
    );

    $encodedText = ""; // Stores the encoded text
    foreach (str_split($text) as $c) { // Going through each character
        if (array_key_exists($c, $MORSE_CODE)) { // Checks if it is a valid character
            $encodedText .= $MORSE_CODE[$c] . " "; // Appends the correct character
        } else {
            throw new \Exception("Invalid character: $c");
        }
    }
    substr_replace($encodedText ,"", -1); // Removes trailing space
    return $encodedText;
}

/**
 * Decode Morse Code to text.
 * @param string text to decode
 */
function decode(string $text): string
{
    $MORSE_CODE = array( // An array containing morse code to text translations
        ".-" => "A",
        "-..." => "B",
        "-.-." => "C",
        "-.." => "D",
        "." => "E",
        "..-." => "F",
        "--." => "G",
        "...." => "H",
        ".." => "I",
        ".---" => "J",
        "-.-" => "K",
        ".-.." => "L",
        "--" => "M",
        "-." => "N",
        "---" => "O",
        ".--." => "P",
        "--.-" => "Q",
        ".-." => "R",
        "..." => "S",
        "-" => "T",
        "..-" => "U",
        "...-" => "V",
        ".--" => "W",
        "-..-" => "X",
        "-.--" => "Y",
        "--.." => "Z",
        ".----" => "1",
        "..---" => "2",
        "...--" => "3",
        "....-" => "4",
        "....." => "5",
        "-...." => "6",
        "--..." => "7",
        "---.." => "8",
        "----." => "9",
        "-----" => "0",
        "/" => " "
    );

    $decodedText = ""; // Stores the decoded text
    foreach (explode(" ", $text) as $c) { // Going through each group
        if (array_key_exists($c, $MORSE_CODE)) { // Checks if it is a valid character
            $decodedText .= $MORSE_CODE[$c]; // Appends the correct character
        } else {
            if ($c) { // Makes sure that the string is not empty to prevent trailing spaces or extra spaces from breaking this
                throw new \Exception("Invalid character: $c");
            }
        }
    }
    return $decodedText;
}


<?php

/**
 * The XOR cipher is a type of additive cipher.
 * Each character is bitwise XORed with the key.
 * We loop through the input string, XORing each
 * character with the key.
 * The key is repeated until it is the same length as the input.
 *
 * @param string $input The input string.
 * @param string $key The key to use.
 * @return string The encrypted string.
 */
function xorCipher(string $input_string, string $key)
{

    $key_len = strlen($key);
    $result = array();

    for ($idx = 0; $idx < strlen($input_string); $idx++)
    {
        array_push($result, $input_string[$idx] ^ $key[$idx % $key_len]);
    }

    return join("", $result);
}


<?php

/**
 * This function converts the
 * submitted Binary Number to
 * Decimal Number.
 *
 * Working of Algorithm
 * (10) base 2
 * (1 * (2 ^ 1) + 0 * (2 ^ 0)) base 10
 * (2 + 0) base 10
 * 2 base 10
 *
 * @param string $binaryNumber
 * @return int
 */
function binaryToDecimal($binaryNumber)
{
    if (!is_numeric($binaryNumber)) {
        throw new \Exception('Please pass a valid Binary Number for Converting it to a Decimal Number.');
    }

    $decimalNumber = 0;
    $binaryDigits  = array_reverse(str_split($binaryNumber));

    foreach ($binaryDigits as $index => $digit) {
        $decimalNumber += $digit * pow(2, $index);
    }

    return $decimalNumber;
}


<?php

/**
 * This function converts the
 * submitted Decimal Number to
 * Binary Number.
 *
 * @param string $decimalNumber
 * @return string
 */
function decimalToBinary($decimalNumber)
{
    if (!is_numeric($decimalNumber)) {
        throw new \Exception('Please pass a valid Decimal Number for Converting it to a Binary Number.');
    }

    $binaryNumber = '';

    while ($decimalNumber > 0) {
        $binaryNumber = ($decimalNumber % 2) . $binaryNumber;
        $decimalNumber /= 2;
    }

    return $binaryNumber;
}


<?php

/**
 * This function converts the
 * submitted Octal Number to
 * Decimal Number.
 *
 * Working of Algorithm
 * (AB) base 16
 * (A * (16 ^ 1) + B * (16 ^ 0)) base 10
 * (10 * (16 ^ 1) + 11 * (16 ^ 0)) base 10
 * (160 + 11) base 10
 * 171 base 10
 *
 * @param string $octalNumber
 * @return int
 */
function hexToDecimal($hexNumber)
{
    // Using ctype to check all the digits are valid hex digits or not.
    if (!ctype_xdigit($hexNumber)) {
        throw new \Exception('Please pass a valid Hexadecimal Number for Converting it to a Decimal Number.');
    }

    $decimalNumber = 0;

    // Mapping for Decimal Digits after 9
    $decimalDigitMappings = [
        'A' => 10,
        'B' => 11,
        'C' => 12,
        'D' => 13,
        'E' => 14,
        'F' => 15,
    ];

    $hexDigits = str_split($hexNumber);
    $hexDigits = array_reverse($hexDigits);

    foreach ($hexDigits as $power => $digit) {
        $hexDigit = $digit;
        if (!is_numeric($digit)) {
            $hexDigit = $decimalDigitMappings[$digit];
        }
        $decimalNumber += (pow(16, $power) * $hexDigit);
    }
    return $decimalNumber;
}

/**
 * This function converts the
 * submitted Decimal Number to
 * Hexadecimal Number.
 *
 * @param string $decimalNumber
 * @return string
 */
function decimalToHex($decimalNumber)
{
    $hexDigits = [];

    // Mapping for HexaDecimal Digits after 9
    $hexDigitMappings = [
        10 => 'A',
        11 => 'B',
        12 => 'C',
        13 => 'D',
        14 => 'E',
        15 => 'F',
    ];
    if (!is_numeric($decimalNumber)) {
        throw new \Exception('Please pass a valid Decimal Number for Converting it to a Hexadecimal Number.');
    }

    while ($decimalNumber > 0) {
        $remainder = ($decimalNumber % 16);
        $decimalNumber /= 16;
        if (empty($hexDigits) && 0 === $remainder) {
            continue;
        }
        $hexDigits[] = $remainder;
    }

    $hexDigits = array_reverse($hexDigits);

    foreach ($hexDigits as $index => $digit) {
        if ($digit > 9) {
            $hexDigits[$index] = $hexDigitMappings[$digit];
        }
    }

    $hexNumber = ltrim(implode('', $hexDigits), '0'); // Connecting all the digits and removing leading zeroes.

    return $hexNumber;
}


<?php

/**
 * This function converts the
 * submitted Octal Number to
 * Decimal Number.
 *
 * Working of Algorithm
 * (10) base 8
 * (1 * (8 ^ 1) + 0 * (8 ^ 0)) base 10
 * (8 + 0) base 10
 * 9 base 10
 * @param string $octalNumber
 * @return int
 */
function octalToDecimal($octalNumber)
{
    if (!is_numeric($octalNumber)) {
        throw new \Exception('Please pass a valid Octal Number for Converting it to a Decimal Number.');
    }

    $decimalNumber = 0;
    $octalDigits   = array_reverse(str_split($octalNumber));

    foreach ($octalDigits as $index => $digit) {
        $decimalNumber += $digit * pow(8, $index);
    }

    return $decimalNumber;
}

/**
 * This function converts the
 * submitted Decimal Number to
 * Octal Number.
 *
 * @param string $decimalNumber
 * @return string
 */
function decimalToOctal($decimalNumber)
{
    if (!is_numeric($decimalNumber)) {
        throw new \Exception('Please pass a valid Decimal Number for Converting it to an Octal Number.');
    }

    $octalNumber = '';

    while ($decimalNumber > 0) {
        $octalNumber = ($decimalNumber % 8) . $octalNumber;
        $decimalNumber /= 8;
    }

    return $octalNumber;
}


<?php

/**
 * This function converts the submitted
 * speed from one unit to another
 *
 * Conversion explanation taken from
 * https://www.asknumbers.com/SpeedConversion.aspx
 * Available units for conversion:
 * mph -> miles per hour
 * km/h -> kilometers per hour
 * m/s -> meters per second
 * ft/s -> feet per second
 * kn -> 1 knot which is equal to 1 nautical mile (1852 km/h)
 * The conversion is made using kilometers as base
 *
 * @param float $speed
 * @param string $unitFrom
 * @param string $unitTo
 * @return int
 */
function convertSpeed(float $speed, string $unitFrom, string $unitTo)
{
    $speedUnitsFrom = [
        'mph' => 1.609344,
        'km/h' => 1,
        'm/s'=> 3.6,
        'ft/s'=> 1.097,
        'kn' => 1.852,
    ];
    $speedUnitsTo = [
        'mph' => 0.6213712,
        'km/h' => 1,
        'm/s'=> 0.277778,
        'ft/s'=> 0.911344,
        'kn' => 0.539957,
    ];
    $availableUnits = array_keys($speedUnitsFrom);

    if (!is_numeric($speed)) {
        throw new \Exception("Please pass a valid speed number for converting it from one unit to another.");
    }
    if (!in_array($unitFrom, $availableUnits) || !in_array($unitTo, $availableUnits)) {
        throw new \Exception("Please pass a valid speed unit.\n\nAvailable units: " . implode(', ', $availableUnits));
    }

    return round($speed * $speedUnitsFrom[$unitFrom] * $speedUnitsTo[$unitTo], 2);
}


<?php
/**
 *  Singly Linked List
 */

class SinglyLinkedList
{
    public ?SinglyLinkedList $next = null;
    public $data;

    public function __construct($data)
    {
        $this->data = $data;
    }

    public function append($data): void
    {
        $current = $this;

        while ($current instanceof SinglyLinkedList && isset($current->next)) {
            $current = $current->next;
        }

        $current->next = new SinglyLinkedList($data);
    }

    public function delete($data): SinglyLinkedList
    {
        $current = $this;

        if ($current->data == $data) {
            return $current->next;
        }

        while ($current instanceof SinglyLinkedList && isset($current->next)) {
            if ($current->next->data === $data) {
                $current->next = $current->next->next;

                return $this;
            }

            $current = $current->next;
        }

        return $this;
    }
}


<?php

/**
 * Breadth-first search (BFS) is an algorithm for searching a tree data structure for a node that satisfies a given property.
 * (https://en.wikipedia.org/wiki/Breadth-first_search).
 *
 * This is a non recursive implementation.
 *
 * References:
 * https://cp-algorithms.com/graph/breadth-first-search.html
 *
 * https://the-algorithms.com/algorithm/depth-first-search?lang=python
 *
 * @author Aryansh Bhargavan https://github.com/aryanshb
 * @param array $adjList An array representing the grapth as an Adjacent List
 * @param int|string $start The starting vertex
 * @return bool if path between start and end vertex exists
 */

function bfs($adjList, $start, $end, $yes = false){
    $visited = [];
    $queue = [$start];
    while (!empty($queue)) {
        $v = array_shift($queue);
        $visited[$v] = 1;
        foreach ($adjList[$v] as $adj) {
            if (!array_key_exists($adj, $visited)) {
                array_push($queue, $adj);
            }
        }
    }
    // return array_key_exists($end, $visited);
    if (array_key_exists($end, $visited)) {
        $yes = true;
    }
    return $yes;
}


<?php

/**
 * The Depth-first Search is an algorithm used for traversing or searching trees or graphs
 * (https://en.wikipedia.org/wiki/Depth-first_search).
 *
 * This is a non recursive implementation.
 *
 * References:
 * https://www.geeksforgeeks.org/depth-first-search-or-dfs-for-a-graph/
 *
 * https://the-algorithms.com/algorithm/depth-first-search?lang=python
 *
 * @author Andre Alves https://github.com/andremralves
 * @param array $adjList An array representing the grapth as an Adjacent List
 * @param int|string $start The starting vertex
 * @return array The visited vertices
 */
function dfs($adjList, $start)
{
    $visited = [];
    $stack = [$start];
    while (!empty($stack)) {
        // Removes the ending element from the stack and mark as visited
        $v = array_pop($stack);
        $visited[$v] = 1;

        // Checks each adjacent vertex of $v and add to the stack if not visited
        foreach (array_reverse($adjList[$v]) as $adj) {
            if (!array_key_exists($adj, $visited))
                array_push($stack, $adj);
        }
    }
    return array_keys($visited);
}


<?php
/**
 * This function calculates
 * Absolute max values from
 * the different numbers
 * provided
 *
 * @param decimal $numbers A variable sized number input
 * @return decimal $absoluteMax Absolute max value
 */
function absolute_max(...$numbers)
{
    if (empty($numbers)) {
        throw new \Exception('Please pass values to find absolute max value');
    }

    $absoluteMax = $numbers[0];
    for ($loopIndex = 0; $loopIndex < count($numbers); $loopIndex++) {
        if ($numbers[$loopIndex] > $absoluteMax) {
            $absoluteMax = $numbers[$loopIndex];
        }
    }

    return $absoluteMax;
}


<?php
/**
 * This function calculates
 * Absolute min values from
 * the different numbers
 * provided.
 *
 * @param decimal $numbers A variable sized number input
 * @return decimal $absoluteMin Absolute min value
 */
function absolute_min(...$numbers)
{
    if (empty($numbers)) {
        throw new \Exception('Please pass values to find absolute min value');
    }

    $absoluteMin = $numbers[0];
    for ($loopIndex = 0; $loopIndex < count($numbers); $loopIndex++) {
        if ($numbers[$loopIndex] < $absoluteMin) {
            $absoluteMin = $numbers[$loopIndex];
        }
    }

    return $absoluteMin;
}


<?php
/**
 * This function checks if given number is Armstrong
 * e.g. 153
 * @param integer $input
 * @return boolean
 */
function isNumberArmstrong(int $input): bool
{
  $arr = array_map('intval', str_split($input));
  $sumOfCubes = 0;
  foreach ($arr as $num) {
    $sumOfCubes += $num * $num * $num;
  }
  if ($sumOfCubes == $input) {
    return true;
  }
  else {
    return false;
  }
}
?>


<?php
/**
 * This function checks if given number is palindromic
 * e.g. 121
 * @param integer $input
 * @return boolean
 */
function isNumberPalindromic(int $input): bool
{
  $arr = array_map('intval', str_split($input));
  $arrRev = array_reverse($arr);
  $inputRev = (int)implode("", $arrRev);
  if ($input == $inputRev) {
    return true;
  }
  else {
    return false;
  }
}
?>


<?php
/**
 * This function check whether
 * the provided integer is a prime
 * number or not.
 *
 * @param Integer $number An integer input
 * @return boolean whether the number is prime or not
 */
function isPrime(int $number)
{
    if ($number === 2) {
        return true;
    }

    if ($number % 2 === 0 or $number < 2) {
        return false;
    }

    $i = 3;
    while ($i <= sqrt($number)) {
        if ($number % $i === 0) {
            return false;
        }
        $i += 2;
    }

    return true;
}


<?php
/**
 * This function calculates
 * and returns the factorial
 * of provided positive integer
 * number.
 *
 * @param Integer $number Integer input
 * @return Integer Factorial of the input
 */
function factorial(int $number)
{
    static $cache = [];//internal caching memory for speed

    if ($number < 0) {
        throw new \Exception("Negative numbers are not allowed for calculating Factorial");
    }
    if ($number === 0) {
        return 1; // Factorial of 0 is 1
    }

    if (isset($cache[$number])) {
        return $cache[$number];
    }

    $fact = ($number * factorial($number - 1)); // Recursion since x! = x * (x-1)!
    $cache[$number] = $fact;

    return $fact;
}


<?php
/**
 * This functions computes an exponent by doing repeated squares
 *
 * @param Integer $base the base
 * @param Integer $exponent the exponent (ie the number of times that the base is multiplied by itself)
 * @return Integer
 */
function fastExponentiation(int $base, int $exponent)
{
    if ($exponent == 0) {
		return 1;
	}

	if ($exponent == 1) {
		return $base;
	}

	if ($exponent % 2 == 0) {
		return fastExponentiation($base*$base, $exponent/2);
	} else {
		return $base * fastExponentiation($base*$base, ($exponent-1)/2);
	}
}


<?php

/**
 * Fibonacci recursive
 *
 * @param int $num
 * @return int
 */
function fibonacciRecursive(int $num)
{
    /*
     * Fibonacci series using recursive approach
     */

    $fibonacciRecursive = [];
    for ($i = 0; $i < $num; $i++) {
        array_push($fibonacciRecursive, recursive($i));
    }
    return $fibonacciRecursive;
}

/**
 * @param int $num
 * @return int
 */
function recursive(int $num)
{
    if ($num < 0) {
        throw new \Exception("Number must be greater than 0.");
    } else {
        if ($num == 0 || $num == 1) {
            return $num;
        } else {
            return recursive($num - 1) + recursive($num - 2);
        }
    }
}

function fibonacciWithBinetFormula(int $num)
{
    /*
     * Fibonacci series using Binet's formula given below
     * binet's formula =  ((1 + sqrt(5) / 2 ) ^ n - (1 - sqrt(5) / 2 ) ^ n ) ) / sqrt(5)
     * More about Binet's formula found at http://www.maths.surrey.ac.uk/hosted-sites/R.Knott/Fibonacci/fibFormula.html#section1
     */

    $fib_series = [];

    if ($num < 0) {
        throw new \Exception("Number must be greater than 0.");
    } else {
        $sqrt = sqrt(5);
        $phi_1 = (1 + $sqrt) / 2;
        $phi_2 = (1 - $sqrt) / 2;

        foreach (range(0, $num - 1) as $n) {
            $seriesNumber = (pow($phi_1, $n) - pow($phi_2, $n)) / $sqrt;
            array_push($fib_series, (int)$seriesNumber);
        }

    }

    return $fib_series;
}


<?php

/*
 * Print Fibocanni sequence using a generator
 */

 /**
  * @param int $i number of Fibonacci numbers to generate
  * @param Generator $set Fibonacci calculator
  */
function loop($i, Generator $set)
{
    while($i-- > 0 && $set->valid())
    {
        yield $set->current();
        $set->next();
    }
}

/*
 * Fibonacci generator
 */
function fib()
{
    yield $i = 0;
    yield $j = 1;

    while(true)
    {
        yield $k = $i + $j;
        $i = $j;
        $j = $k;
    }
}

/*
 * Generate 100 Fibonacci numbers
 */
foreach(loop(100, fib()) as $item)
{
    print($item.',');
}


<?php
/**
 * This function calculates
 * The mean value of
 * numbers provided
 *
 * @param decimal $numbers A variable sized number input
 * @return decimal $mean Mean of provided numbers
 */
function mean(...$numbers)
{
    if (empty($numbers)) {
        throw new \Exception('Please pass values to find mean value');
    }

    $total = array_sum($numbers);
    $mean = $total / count($numbers);

    return $mean;
}


<?php
/**
 * This function calculates
 * The median value of
 * numbers provided
 *
 * @param decimal $numbers A variable sized number input
 * @return decimal $median Median of provided numbers
 */
function median(...$numbers)
{
    if (empty($numbers)) {
        throw new \Exception('Please pass values to find mean value');
    }

    sort($numbers);
    $length = count($numbers);

    $middle = ceil($length / 2);
    if($length % 2 == 0) {
        return ($numbers[$middle] + $numbers[$middle - 1]) / 2;
    }

    return $numbers[$middle - 1];
}


<?php
/**
 * This function calculates
 * The mode value of
 * numbers provided
 *
 * @param decimal $numbers A variable sized number input
 * @return decimal $mode Mode of provided numbers
 */
function mode(...$numbers)
{
    if (empty($numbers)) {
        throw new \Exception('Please pass values to find mean value');
    }

    $values = array_count_values($numbers);
    $mode = array_search(max($values), $values);

    return $mode;
}


<?php
function isNumberNeon($input)
{
  $inputSquare = $input * $input;
  $inputArr = array_map('intval', str_split($inputSquare));
  $sumOfSquareDigits =0;
  foreach ($inputArr as $digit) {
    $sumOfSquareDigits +=$digit;
  }
  return $sumOfSquareDigits ==$input;
}
?>


<?php
/**
 * This function check whether
 * the provided number is a
 * perfect square or not.
 *
 * @param Decimal $number A decimal input
 * @return boolean whether the number is perfect square or not
 */
function isPerfectSquare($number)
{
    $root = (int) sqrt($number);
    return (($root * $root) === $number); // If number's square root is an integer then it's a perfect square else not
}


<?php

/**
 * Problem:
 *
 * If we list all the natural numbers below 10 that are multiples of 3 or 5,
 * we get 3, 5, 6 and 9. The sum of these multiples is 23.
 *
 * Find the sum of all the multiples of 3 or 5 below 1000.
 *
 *
 * Answer:
 *
 * 233168
 *
 *
 * @link https://projecteuler.net/problem=1
 */

/**
 * @return int
 */
function problem1a(): int
{
    $maxNumber = 999; // below 1000

    $numbers = range(1, $maxNumber);

    return array_reduce($numbers, function($carry, $number) {
        $shouldCarry = $number % 3 == 0 || $number % 5 == 0;
        return $carry += $shouldCarry ? $number : 0;
    });
}

/**
 * @return int
 */
function problem1b(): int
{
    $maxNumber = 999; // below 1000

    $numbersMultByThree = range(3, $maxNumber, 3);
    $numbersMultByFive = range(5, $maxNumber, 5);

    $numbers = array_merge($numbersMultByThree, $numbersMultByFive);

    return array_sum(array_unique($numbers));
}


<?php
/*
 * This function solves the problem 10 of the Project Euler.
 *
 * Problem description:
 * The sum of the primes below 10 is 2 + 3 + 5 + 7 = 17.
 * Find the sum of all the primes below two million.
 */

/**
 * @return int
 */
function problem10(): int
{
    //Using Sieve of Erastothenes
    //@see https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes

    $n = 2000000;
    $isPrime = [];

    for ($i = 2; $i <= $n; $i++){
        $isPrime[$i] = $i;
    }

    for ($i = 2; $i*$i <= $n; $i++){
        if (isset ($isPrime[$i])){
            for ( $j = $i; $i*$j <= $n; $j++)
               unset($isPrime[$i*$j]);
        }
    }

    return array_sum($isPrime);
}


<?php

/**
 * Problem:
 *
 * Each new term in the Fibonacci sequence is generated by adding the previous
 * two terms. By starting with 1 and 2, the first 10 terms will be:
 *
 * 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...
 *
 * By considering the terms in the Fibonacci sequence whose values do not
 * exceed four million, find the sum of the even-valued terms.
 *
 *
 * Answer:
 *
 * 4613732
 *
 *
 * @link https://projecteuler.net/problem=2
 */

/**
 * @return int
 */
function problem2(): int
{
    $maxNumber = 4000000;
    $currNumber = 1;
    $nextNumber = 2;
    $answer = 0;

    while ($currNumber <= $maxNumber) {
        $answer += $currNumber % 2 == 0 ? $currNumber : 0;
        [$currNumber, $nextNumber] = [$nextNumber, $currNumber + $nextNumber];
    }

    return $answer;
}


<?php

/**
 * Problem:
 *
 * The prime factors of 13195 are 5, 7, 13 and 29.
 *
 * What is the largest prime factor of the number 600851475143?
 *
 *
 * Answer:
 *
 * 6857
 *
 *
 * @link https://projecteuler.net/problem=3
 */

/**
 * @return int
 */
function problem3(): int
{
    $n = 600851475143;
    $i = 2;
    while ($i * $i < $n) {
        while ($n % $i == 0) {
            $n /= $i;
        }
        $i++;
    }

    return $n;
}


<?php

/**
 * Problem:
 * A palindromic number reads the same both ways. The largest palindrome made from the product of two 2-digit numbers is 9009 = 91 Ã— 99.
 * Find the largest palindrome made from the product of two 3-digit numbers.
 */

/**
 * @return int
 */
function problem4(): int
{
    $largest  = 0;

    for ($i=100; $i<1000; $i++){

        for ($j=$i; $j<1000; $j++) {
            $product = $i*$j;

            if ( strrev((string)$product) == (string)$product && $product > $largest) {
                $largest = $product;
            }
        }
    }

    return $largest;
}


<?php

/**
 * This function solves the problem 5 of the Project Euler.
 *
 * Problem description:
 * 2520 is the smallest number that can be divided by each of the numbers from 1 to 10 without any remainder.
 * What is the smallest positive number that is evenly divisible by all of the numbers from 1 to 20?
 */

/**
 * @return int
 */
function problem5(): int
{
    $number = 20;
    while (true) {
        $isSolution = true;
        for ($i = 11; $i <= 20; $i++) {
            if ($number % $i !== 0) {
                $isSolution = false;
                break;
            }
        }
        if ($isSolution) {
            return $number;
        }
        $number += 20;
    }
}


<?php

/**
 * This function solves the problem 6 of the Project Euler.
 *
 * Problem description:
 * The sum of the squares of the first ten natural numbers is,
 *              1 ** 2 + 2 ** 2 + ... + 10 ** 2 = 385
 *
 * The square of the sum of the first ten natural numbers is,
 *              (1 + 2 + ... + 10) ** 2 = 3025
 *
 * Hence the difference between the sum of the squares of the
 * first ten natural numbers and the square of the sum is .
 *              3025 - 385 = 2640
 *
 * Find the difference between the sum of the squares of the
 * first one hundred natural numbers and the square of the sum.
 *
 * Answer: 25164150
 *
 * @param int $maxNumber Range from 1 to <maxNumber>
 *
 * @return int Difference between the sum of the squares and
 * the square of the sum
 *
 * @link https://projecteuler.net/problem=6
 */

/**
 * @param int $maxNumber Range from 1 to <maxNumber>
 *
 * @return int Difference between the sum of the squares and
 * the square of the sum
 */
function problem6(int $maxNumber = 100): int
{
    $sumOfSquares = 0;
    $sums = 0;
    for ($i = 1; $i <= $maxNumber; $i++) {
        $sumOfSquares += $i ** 2; // add squares to the sum of squares
        $sums += $i; // add number to sum to square later
    }
    return ($sums ** 2) - $sumOfSquares; // difference of square of the total sum and sum of squares
}


<?php

/**
 * This function solves the problem 7 of the Project Euler.
 *
 * Problem description:
 * By listing the first six prime numbers: 2, 3, 5, 7, 11, and 13, we can see that the 6th prime is 13.
 * What is the 10 001st prime number?
 */

/**
 * @return int
 */
function problem7(): int
{
    $numberOfPrimes = 0;
    $number = 0;
    while ($numberOfPrimes < 10001) {
        $number++;
        if (isPrime($number)) {
            $numberOfPrimes++;
        }
    }
    return $number;
}


<?php

/**
 * This function solves the problem 9 of the Project Euler.
 *
 * Problem description:
 * A Pythagorean triplet is a set of three natural numbers, a < b < c, for which,
 *
 * a^2 + b^2 = c^2
 *
 * For example:
 *
 * 3^2 + 4^2 = 9 + 16 = 25 = 5^2.
 *
 * There exists exactly one Pythagorean triplet for which a + b + c = 1000. Find the product abc.
 */

/**
 * @return int
 */
function problem9(): int
{
    for ($i = 0; $i <= 300; $i++) {
        for ($j = 0; $j <= 400; $j++) {
            $k = 1000 - $i - $j;
            if ($i * $i + $j * $j === $k * $k) {
                return $i * $j * $k;
            }
        }
    }
    return 0;
}


<?php

/**
 * Binary search algorithm iterative approach
 *
 * Be careful collection must be ascending sorted, otherwise result will be unpredictable
 *
 * Examples:
 * binarySearchIterative([0, 5, 7, 10, 15], 0);
 * 0
 * binarySearchIterative([0, 5, 7, 10, 15], 15);
 * 4
 * binarySearchIterative([0, 5, 7, 10, 15], 5);
 * 1
 * binarySearchIterative([0, 5, 7, 10, 15], 6);
 *
 * @param array $list
 * @param int $target
 * @return int
 */
function binarySearchIterative($list, $target)
{
    $first = 0;
    $last = count($list)-1;

    while ($first <= $last) {
        $mid = ($first + $last) >> 1;

        if ($list[$mid] == $target) {
            return $mid;
        } elseif ($list[$mid] > $target) {
            $last = $mid - 1;
        } elseif ($list[$mid] < $target) {
            $first = $mid + 1;
        }
    }

    return null;
}

/**
 * Binary search algorithm in PHP by recursion
 *
 * Be careful collection must be ascending sorted, otherwise result will be unpredictable
 *
 * Examples:
 * binarySearchByRecursion([0, 5, 7, 10, 15], 0, 0, 4);
 * 0
 * binarySearchByRecursion([0, 5, 7, 10, 15], 15, 0, 4);
 * 4
 * binarySearchByRecursion([0, 5, 7, 10, 15], 5, 0, 4);
 * 1
 * binarySearchByRecursion([0, 5, 7, 10, 15], 6, 0, 4);
 *
 * @param Array $list a sorted array list of integers to search
 * @param integer $target an integer number to search for in the list
 * @param integer $start an integer number where to start searching in the list
 * @param integer $end an integer number where to end searching in the list
 * @return integer the index where the target is found (or null if not found)
 */
function binarySearchByRecursion($list, $target, $start, $end)
{
    if (count($list) == 0) {
        return null;
    }

    if (count($list) < 2) {
        return $list[0] == $target ? 0 : null;
    }

    if ($start > $end)
        return null;

    $mid = ($start + $end) >> 1;

    if ($list[$mid] == $target) {
        return $mid;
    } elseif ($list[$mid] > $target) {
        return binarySearchByRecursion($list, $target, $start, $mid-1);
    } elseif ($list[$mid] < $target) {
        return binarySearchByRecursion($list, $target, $mid+1, $end);
    }

    return null;
}


<?php
/*
 * Exponential Search Algorithm
 *
 * The algorithm consists of two stages.
 * The first stage determines a range in which the search key would reside
 **** if it were in the list.
 * In the second stage, a binary search is performed on this range.
 */
 /**
  * @param Array $arr
  * @param int $value
  * @param int $floor
  * @param int $ceiling
  * @return int
  **/
function binarySearch ($arr, $value, $floor, $ceiling) {
  // Get $middle index
  $mid = floor(($floor + $ceiling) / 2);
  // Return position if $value is at the $mid position
  if ($arr[$mid] === $value) {
    return (int) $mid;
  }
  //Return -1 is range is wrong
  if ($floor > $ceiling) return -1;
  // search the left part of the $array
  // If the $middle element is great than the $value
  if ($arr[$mid] > $value) {
    return binarySearch($arr, $value, $floor, $mid - 1);
  }
  // search the right part of the $array
  // If the $middle element is lower than the $value
  else {
    return binarySearch($arr, $value, $mid + 1, $ceiling);
  }
}
 /**
  * @param Array $arr
  * @param int $length
  * @param int $value
  * @return int
  **/
function exponentialSearch ($arr, $value) {
  // If $value is the first element of the $array return this position
  if ($arr[0] === $value) {
    return 0;
  }

  // Find range for binary search
  $i = 1;
  $length = count($arr);
  while ($i < $length && $arr[$i] <= $value) {
    $i = $i * 2;
  }
  $floor = $i/2;
  $ceiling = min($i, $length);

  // Call binary search for the range found above
  return binarySearch($arr, $value, $floor, $ceiling);
}


<?php

/*
 * Function to find nth number in Fibonacci sequence.
 * Uses a version of memoization and runs very fast!
 */

/**
 * @param int $n position to check
 * @param array $m array to store solved trees
 */
function fibonacciPosition(int $n, array &$m = [])
{
    if(isset($m[$n])) return $m[$n];
    if($n < 2) return $n;

    $m[$n] = fibonacciPosition($n - 1, $m) + fibonacciPosition($n - 2, $m);

    return $m[$n];

}

print fibonacciPosition(59);


<?php
/**
 * Interpolation Search********
 *
 *
 * Description***********
 * Searches for a key in a sorted array
 *
 *
 * How************
 * Loop through the array:
 * Determine the index from the low and high indices
 * if the (value of index in array) is = key return the "POSITION", end loop
 * if the (value of index in array) is < key increase the low index
 * if the (value of index in array) is > key decrease the high index
 * repeat the loop
 */
function interpolationSearch($arr, $key) {
  $length = count($arr) - 1;
  $low = 0;
  $high = $length;
  $position = -1;
  //loop, between low & high
  while ($low <= $high && $key >= $arr[$low] && $key <= $arr[$high]) {
    //GET INDEX
    $delta = ($key - $arr[$low]) / ($arr[$high] - $arr[$low]);
    $index = $low + floor(($high - $low) * $delta);
    //GET VALUE OF INDEX IN ARRAY...
    $indexValue = $arr[$index];
    if ($indexValue === $key) {
      //index value equals key
      //FOUND TARGET
      //return index value
      $position = $index;
      return (int) $position;
    }
    if ($indexValue < $key) {
      //index value lower than key
      //increase low index
      $low = $index + 1;
    }
    if ($indexValue > $key) {
      //index value higher than key
      //decrease high index
      $high = $index - 1;
    }
  }
  //when key not found in array or array not sorted
  return null;
}


<?php
/**
   * Jump Search algorithm in PHP
   * References: https://www.geeksforgeeks.org/jump-search/
   * The list must be sorted in ascending order before performing jumpSearch
   *
   * @param Array $list refers to a sorted list of integer
   * @param integer $key refers to the integer target to be searched from the sorted list
   * @return index of $key if found, otherwise -1 is returned
 */

function jumpSearch($list,$key)
{
	/*number of elements in the sorted array*/
	$num = count($list);
	/*block size to be jumped*/
	$step = (int)sqrt($num);
	$prev = 0;

	while ($list[min($step, $num)-1] < $key)
	{
		$prev = $step;
		$step += (int)sqrt($num);
		if ($prev >= $num)
			return -1;
	}

	/*Performing linear search for $key in block*/
	while ($list[$prev] < $key)
	{
		$prev++;
		if ($prev == min($step, $num))
			return -1;
	}

     return $list[$prev] === $key ? $prev : -1;
}


<?php

/**
 * Linear search in PHP
 *
 * Reference: https://www.geeksforgeeks.org/linear-search/
 *
 * @param Array $list a array of integers to search
 * @param integer $target an integer number to search for in the list
 * @return integer the index where the target is found (or -1 if not found)
 *
 * Examples:
 *  data =  5, 7, 8, 11, 12, 15, 17, 18, 20
 *  x = 15
 *  Element found at position 6
 *
 *  x = 1
 *  Element not found
 *
 *  @param Array $list a array of integers to search
 *  @param integer $target an integer number to search for in the list
 *  @return integer the index where the target is found (or -1 if not found)
 */
function linearSearch($list, $target)
{
    $n = sizeof($list);
    for($i = 0; $i < $n; $i++)
    {
        if($list[$i] == $target) {
            return $i + 1;
        }
    }

    return -1;
}


<?php

require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../Utils/ArrayHelpers.php';

/**
 * Lower Bound of an element is minimum index that an element would be placed
 * if it is added into that sorted array
 *
 * [C++ Lower Bound](http://www.cplusplus.com/reference/algorithm/lower_bound/)
 *
 * It is assumed that an integer array is provided
 * and the second parameter is also a integer
 *
 * @param array of sorted integers
 * @param integer whose lower bound is to be found
 *
 * @return the index of lower bound of the given element
 */
function lowerBound(array $arr, int $elem){
    isSortedAscendingInts($arr);
    $hi = count($arr);
    $lo = 0;

    while($lo < $hi){
        $mid = $lo + floor(($hi - $lo)/2);

        if($arr[$mid] < $elem){
            $lo = $mid+1;
        }else{
            $hi = $mid;
        }
    }

    return $lo;
}


<?php
/* Ternary search is similar to binary search
 * It rather divides the sorted array into three parts rather than.two parts by using two middle points, $mid1  $mid2.
 * The value of the $key will first be compared with the two $mid points, the value will be returned if there is a match.
 * Then, if the value of the $key is less than $mid1, narrow the interval to the first part.
 * Else, if the value of the $key is greater than $mid2, narrow the interval to the third part.
 * Otherwise, narrow the interval to the $middle part.
 * Repeat the steps until the value is found or the interval is empty (value not found after checking all elements).
 */

function ternarySearchByRecursion ($arr, $key, $low, $high) {

    //Return -1 if high lesser than low, we can't find item in the whole array
    if ($high < $low) {
      return null;
    }

    //get $mid1 and $mid2
    $mid1 = floor($low + ($high - $low) / 3);
    $mid2 = floor($high - ($high - $low) / 3);
    // check if $key is found at any $mid
    if ($arr[$mid1] === $key) {
      // return index of $key if found
      return $mid1;
    }
    if ($arr[$mid2] === $key) {
      // return index of $key if found
      return $mid2;
    }

    // since the $key is not found at $mid,
    // check in which region it is present
    // and repeat the Search operation
    // in that region
    if ($key < $arr[$mid1]) {
      // the $key lies in between $low and $mid1
      return ternarySearchByRecursion($arr, $key, $low, $mid1 - 1);
    } else if ($key > $arr[$mid2]) {
      // the $key lies in between $mid2 and $high
      return ternarySearchByRecursion($arr, $key, $mid2 + 1, $high);
    } else {
      // the $key lies in between $mid1 and $mid2
      return ternarySearchByRecursion($arr, $key, $mid1 + 1, $mid2 - 1);
    }
}

function ternarySearchIterative ($arr, $key) {
  $low = 0; $high = count($arr) - 1;
  while ($high >= $low) {
    // find the $mid1 and $mid2
    $mid1 = floor($low + ($high - $low) / 3);
    $mid2 = floor($high - ($high - $low) / 3);

    // check if $key is found at any $mid
    if ($arr[$mid1] === $key) {
      // return index of $key if found
      return $mid1;
    }
    if ($arr[$mid2] === $key) {
      // return index of $key if found
      return $mid2;
    }

    // since the $key is not found at $mid,
    // check in which region it is present
    // and repeat the Search operation
    // in that region
    if ($key < $arr[$mid1]) {
      // the $key lies in between $low and $mid1
      $high = $mid1 - 1;
    } else if ($key > $arr[$mid2]) {
      // the $key lies in between $mid2 and $high
      $low = $mid2 + 1;
    } else {
      // the $key lies in between $mid1 and $mid2
      $low = $mid1 + 1;
      $high = $mid2 - 1;
    }
  }
  // the $key was not found
  return null;
}


<?php

require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../Utils/ArrayHelpers.php';

/**
 * Upper Bound of an element is maximum index that an element would be placed
 * if it is added into that sorted array
 *
 * [C++ Lower Bound](http://www.cplusplus.com/reference/algorithm/upper_bound/)
 *
 * It is assumed that an integer array is provided
 * and the second parameter is also a integer
 *
 * @param array of sorted integers
 * @param integer whose upper bound is to be found
 *
 * @return the index of upper bound of the given element
 */
function upperBound(array $arr, int $elem){
    isSortedAscendingInts($arr);
    $hi = count($arr);
    $lo = 0;

    while($lo < $hi){
        $mid = $lo + floor(($hi - $lo)/2);

        if($arr[$mid] <= $elem){
            $lo = $mid + 1;
        }else{
            $hi = $mid;
        }
    }

    return $lo;
}


<?php
/**
 *  Sort an "Array of objects" or "Array of arrays" by keys
 */

class ArrayKeysSort
{
    public const ORDER_ASC = 'ASC';
    public const ORDER_DESC = 'DESC';

    /**
     * @param $collection
     * @param array $keys
     * @param string $order
     * @param bool $isCaseSensitive
     * @return mixed
     */
    public static function sort(
        $collection,
        array $keys,
        string $order = self::ORDER_ASC,
        bool $isCaseSensitive = false
    ) {
        if (!empty($collection) && !empty($keys)) {
            try {
                usort(
                    $collection,
                    function ($a, $b) use ($keys, $order, $isCaseSensitive) {
                        $pos = 0;
                        do {
                            $key = $keys[$pos];
                            if (is_array($a)) {
                                if (!isset($a[$key]) || !isset($b[$key])) {
                                    $errorMsg = 'The key "' . $key
                                        . '" does not exist in the collection';
                                    throw new Exception($errorMsg);
                                }
                                $item1 = !$isCaseSensitive
                                    ? strtolower($a[$key]) : $a[$key];
                                $item2 = !$isCaseSensitive
                                    ? strtolower($b[$key]) : $b[$key];
                            } else {
                                if (!isset($a->$key) || !isset($b->$key)) {
                                    $errorMsg = 'The key "' . $key
                                        . '" does not exist in the collection';
                                    throw new Exception($errorMsg);
                                }
                                $item1 = !$isCaseSensitive
                                    ? strtolower($a->$key) : $a->$key;
                                $item2 = !$isCaseSensitive
                                    ? strtolower($b->$key) : $b->$key;
                            }
                        } while ($item1 === $item2 && !empty($keys[++$pos]));

                        if ($item1 === $item2) {
                            return 0;
                        } elseif ($order === self::ORDER_ASC) {
                            return ($item1 < $item2) ? -1 : 1;
                        } else {
                            return ($item1 > $item2) ? -1 : 1;
                        }
                    }
                );
            } catch (Exception $e) {
                echo $e->getMessage();
                die();
            }
        }

        return $collection;
    }
}


<?php

/**
 * Bubble Sort
 *
 * @param array $array
 * @return array
 */
function bubbleSort($array) {
    $length = count($array);

    for ($i = $length; $i > 0; $i--) {
        $swapped = true;

        for ($j=0;$j<$i-1;$j++) {
            if ($array[$j] > $array[$j + 1]) {
                $temp = $array[$j];
                $array[$j] = $array[$j + 1];
                $array[$j + 1] = $temp;
                $swapped = false;
            }
        }

        if ($swapped) break;
    }

    return $array;
}


<?php
/*
 * Bubble Sort
 *
 * Compare numbers in an array 2 at a time and switch them if the second number is smaller
 *
 * @param array $input array of random numbers
 * @return array sorted array of numbers
 */
function bubbleSort2(array $input)
{
    // Return nothing if input is empty
    if(!isset($input))
    {
        return [];
    }

    do
    {
        $swapped = false;

        for($i = 0, $count = sizeof($input) - 1; $i < $count; $i++)
        {
            if($input[$i + 1] < $input[$i])
            {
                list($input[$i + 1], $input[$i]) = [$input[$i], $input[$i + 1]];
                $swapped = true;
            }
        }
    }
    while($swapped);

    return $input;
}


<?php

/**
 * @param $array
 * @param $min
 * @param $max
 * @return mixed
 */
function countSort($array, $min, $max)
{
    $count = array();

    for($i = $min; $i <= $max; $i++)
    {
        $count[$i] = 0;
    }

    foreach($array as $number)
    {
        $count[$number]++;
    }

    $z = 0;

    for($i = $min; $i <= $max; $i++) {
        while( $count[$i]-- > 0 ) {
            $array[$z++] = $i;
        }
    }

    return $array;
}


<?php

/**
 * Gnome Sort
 * References:
 * https://www.geeksforgeeks.org/gnome-sort-a-stupid-one/
 *
 * The Gnome algorithm works byÂ locatingÂ the first instance in which two adjoining elements are arranged incorrectly and swaps with each other.
 *
 * @param array $array refers to the array to be sorted
 * @return array
 */

function gnomeSort($array){
    $a = 1;
    $b = 2;

    while($a < count($array)){

        if ($array[$a-1] <= $array[$a]){
            $a = $b;
            $b++;
        }else{
            list($array[$a],$array[$a-1]) = array($array[$a-1],$array[$a]);
            $a--;
            if($a == 0){
                $a = $b;
                $b++;
            }
        }
    }

    return $array;
}


<?php

/**
 * Insertion Sort
 *
 * @param array $array
 * @return array
 */
function insertionSort(array $array)
{
    for ($i = 1; $i < count($array); $i++)
    {
        $currentVal = $array[$i];

        for ($j = $i - 1; $j >= 0 && $array[$j] > $currentVal; $j--)
        {
          $array[$j + 1] = $array[$j];
        }

        $array[$j + 1] = $currentVal;
    }

    return $array;
}


<?php

/**
 * Merge Sort
 *
 * @param array $arr
 * @return array
 */
function mergeSort(array $arr)
{
    if (count($arr) <= 1) {
      return $arr;
    }

    $mid = floor( count($arr) / 2 );
    $leftArray = mergeSort( array_slice($arr, 0, $mid) );
    $rightArray = mergeSort( array_slice($arr, $mid) );

    return merge($leftArray, $rightArray);
}

/**
 * @param array $leftArray
 * @param array $rightArray
 * @return array
 */
function merge(array $leftArray, array $rightArray)
{
    $result = [];
    $i = 0;
    $j = 0;

    while ($i < count($leftArray) && $j < count($rightArray)) {
        if ($rightArray[$j] > $leftArray[$i]) {
            $result[] = $leftArray[$i];
            $i++;
        } else {
            $result[] = $rightArray[$j];
            $j++;
        }
    }

    while ($i < count($leftArray)) {
        $result[] = $leftArray[$i];
        $i++;
    }

    while ($j < count($rightArray)) {
        $result[] = $rightArray[$j];
        $j++;
    }

    return $result;
}


<?php
/**
 * Quick Sort
 * Compare number in an array to the next number and sets to new array (greater than or less than)
 *
 * @param array $input array of random numbers
 * @return array sorted array of numbers
 */
function quickSort(array $input)
{
    // Return nothing if input is empty
    if(!isset($input))
    {
        return [];
    }

    $lt = [];
    $gt = [];

    if(sizeof($input) < 2)
    {
        return $input;
    }

    $key = key($input);
    $shift = array_shift($input);

    foreach($input as $value)
    {
        $value <= $shift ? $lt[] = $value : $gt[] = $value;
    }

    return array_merge(quickSort($lt), [$key => $shift], quickSort($gt));
}


<?php

/**
 * Radix Sort
 *
 * @param array $arr
 * @return array
 */
function radixSort($nums)
{
    $maxDigitsCount = maxDigits($nums);
    for ($k=0; $k < $maxDigitsCount; $k++)
    {
        $digitBucket = array_fill(0,10,[]);

        for ($i=0; $i < count($nums); $i++)
        {
            $digitBucket[getDigit($nums[$i], $k)][] = $nums[$i];
        }

        $nums = concat($digitBucket);
    }

    return $nums;
}

/*
 * Helper functions
 */

/**
 * Get the digits value by it's place
 *
 * @param $num
 * @param $k
 * @return int
 */
function getDigit($num, $i)
{
    return floor(abs($num) / pow(10, $i)) % 10;
}

/**
 * Get the digits count
 *
 * @param $nums
 * @return int
 */
function digitsCount($num)
{
    if ($num == 0) return 1;
    return floor(log10(abs($num))) + 1;
}

/**
 * Get the max digits count
 *
 * @param $nums
 * @return int
 */
function maxDigits($arr)
{
    $maxDigits = 0;

    for ($i=0; $i < count($arr); $i++)
    {
        $maxDigits = max($maxDigits, digitsCount($arr[$i]));
    }

    return $maxDigits;
}

/**
 * Concat the array
 *
 * @param $arr
 * @return array
 */
function concat(array $array) {
    $newArray=[];

    for ($i = 0; $i < count($array); $i++)
    {
      for ($j = 0; $j<count($array[$i]); $j++)
      {
          $newArray[] = $array[$i][$j];
      }
    }
    return $newArray;
}


<?php

/**
 * Selection Sort
 *
 * @param array $array
 * @return array
 */
function selectionSorting(array $array)
{
    $length = count($array);

    for ($i = 0; $i < $length; $i++)
    {
        $lowest = $i;

        for ($j = $i + 1; $j < $length; $j++)
        {
            if ($array[$j] < $array[$lowest])
            {
                $lowest = $j;
            }
        }

        if ($i !==$lowest)
        {
            $temp = $array[$i];
            $array[$i] = $array[$lowest];
            $array[$lowest] = $temp;
        }
    }

    return $array;
}


<?php

/**
 * Checks if two strings are anagrams of each other.
 *
 * @param string $originalString
 * @param string $testString
 * @param bool $caseInsensitive
 * @return bool
 */
function isAnagram(string $originalString, string $testString, bool $caseInsensitive = true)
{
    if ($caseInsensitive)
    {
        $originalString = strtolower($originalString); // Converting string to lowercase for case-insensitive check
        $testString = strtolower($testString);
    }

    // count_chars(string, mode = 1) returns key-value pairs with character as key, frequency as value
    // We can directly compare the arrays in this case
    if (count_chars($originalString, 1) == count_chars($testString, 1))
    {
        return true;
    }
    else
    {
        return false;
    }
}


<?php

/**
 * Checks if a string is a palindrome
 *
 * @param string $string
 * @param bool $caseInsensitive
 * @return bool
 */
function isPalindrome(string $string, bool $caseInsensitive = true)
{
    $string = trim($string); // Removing leading and trailing spaces

    if (empty($string))
    {
        return false; // Returning false for an Empty String
    }

    if ($caseInsensitive)
    {
        $string = strtolower($string); // Converting string to lowercase for case-insensitive check
    }

    $characters = str_split($string);

    for ($i = 0; $i < count($characters); $i++)
    {
        if ($characters[$i] !== $characters[count($characters) - ($i + 1)])
        {
            return false;
        }
    }

    return true;
}


<?php

/**
 * This is a simple way to check palindrome string
 * using php strrev() function
 * make it simple
 *
 * @param string $string
 * @param bool $caseInsensitive
 */
function checkPalindromeString(string $string, bool $caseInsensitive = true): String
{
    //removing spaces
    $string = trim($string);

    if (empty($string))
    {
        throw new \Exception('You are given empty string. Please give a non-empty string value');
    }

    /**
     * for case-insensitive
     * lowercase string conversion
     */
    if ($caseInsensitive)
    {
        $string = strtolower($string);
    }

    if ($string !== strrev($string))
    {
        return $string . " - not a palindrome string." . PHP_EOL;
    }

    return $string . " - a palindrome string." . PHP_EOL;
}


<?php

/**
 * Function returns the total number of consonants present in the given
 * string using a linear search through the string
 *
 * @param string $string
 * @return int
 */
function countConsonants(string $string): Int
{
    if (empty($string)) {
        throw new \Exception('Please pass a non-empty string value');
    }

    $vowels     = ['a', 'e', 'i', 'o', 'u']; // Vowels Set
    $string     = strtolower($string); // For case-insensitive checking

    $consonantCount = 0;

    for ($i = 0; $i < strlen($string); $i++)
    {
      if (!in_array($string[$i], $vowels) &&
        $string[$i] >= 'a' && $string[$i] <= 'z')
        {
            $consonantCount++;
        }
    }

    return $consonantCount;
}


<?php

/**
 * This is a simple way to count sentence
 * using php preg_match_all() function
 *
 * @param string $sentence
 * @return int
 */
function countSentences(string $sentence): int
{
    $sentence = trim($sentence);

    return preg_match_all('/[^\s|^\...](\.|\!|\?)(?!\w)/', $sentence);
}


<?php
/**
 * This function returns the total number of vowels present in
 * the given string using a simple method of looping through
 * all the characters present in the string.
 *
 * @param string $string
 * @return int   $numberOfVowels
 */
function countVowelsSimple(string $string)
{
    if (empty($string))
    {
        throw new \Exception('Please pass a non-empty string value');
    }

    $numberOfVowels = 0;
    $vowels     = ['a', 'e', 'i', 'o', 'u']; // Vowels Set
    $string     = strtolower($string); // For case-insensitive checking
    $characters = str_split($string); // Splitting the string to a Character Array.

    foreach ($characters as $character)
    {
        if (in_array($character, $vowels))
        {
            $numberOfVowels++;
        }
    }

    return $numberOfVowels;
}

/**
 * This function returns the Total number of vowels present in the given
 * string using a regular expression.
 *
 * @param string $string
 * @return int
 */
function countVowelsRegex(string $string)
{
    if (empty($string))
    {
        throw new \Exception('Please pass a non-empty string value');
    }
    $string = strtolower($string); // For case-insensitive checking

    return preg_match_all('/[a,e,i,o,u]/', $string);
}


<?php
/**
 * Find distance (Levenshtein distance)
 *
 * Compute the difference between two sequences, ie, the minimum number of changes
 * to get to $str2 from $str1
 *
 * @param string $str1
 * @param string $str2
 * @return int the minimum number of changes to transform one string into another
 */
function findDistance($str1, $str2)
{
    $lenStr1 = strlen($str1);
    $lenStr2 = strlen($str2);

    if ($lenStr1 == 0) {
        return $lenStr2;
    }

    if ($lenStr2 == 0) {
        return $lenStr1;
    }

    $distanceVectorInit = [];
    $distanceVectorFinal = [];

    for ($i = 0; $i < $lenStr1 + 1; $i++) {
        $distanceVectorInit[] = 0;
        $distanceVectorFinal[] = 0;
    }

    for ($i = 0; $i < $lenStr1 + 1; $i++) {
        $distanceVectorInit[$i] = $i;
    }

    for ($i = 0; $i < $lenStr2; $i++) {
        $distanceVectorFinal[0] = $i + 1;

        // use formula to fill in the rest of the row
        for ($j = 0; $j < $lenStr1; $j++) {
            $substitutionCost = 0;

            if ($str1[$j] == $str2[$i]) {
                $substitutionCost = $distanceVectorInit[$j];
            } else {
                $substitutionCost = $distanceVectorInit[$j] + 1;
            }

            $distanceVectorFinal[$j+1] = min($distanceVectorInit[$j+1] + 1, min($distanceVectorFinal[$j] + 1, $substitutionCost));
        }

        $distanceVectorInit = $distanceVectorFinal;
    }

    return $distanceVectorFinal[$lenStr1];
}


<?php
/**
 * This function returns the character which is repeated maximum number of
 * times in the given string.
 *
 * @param string $string
 * @return string
 */
function maxCharacter(string $string)
{
    if (empty($string)) {
        throw new \Exception('Please pass a non-empty string value');
    }

    $characterCountTable = []; // A variable to maintain the character counts
    $string              = strtolower($string); // For case-insensitive checking
    $characters          = str_split($string); // Splitting the string to a Character Array.

    foreach ($characters as $character) {
        $currentCharacterCount = 1;

        if (isset($characterCountTable[$character])) {
            $currentCharacterCount = $characterCountTable[$character] + 1;
        }

        $characterCountTable[$character] = $currentCharacterCount;
    }

    arsort($characterCountTable);

    return array_keys($characterCountTable)[0];
}


<?php

/**
 * This function returns a given string in reverse order
 *
 * @param string $string
 * @return string
 */
function reverseString(string $string)
{
    $string = trim($string); // Removing leading and trailing spaces

    $characters = str_split($string);

    $reversedCharacters = [];

    for ($i = (count($characters) - 1); $i >= 0; $i--) {
        $reversedCharacters[] = $characters[$i];
    }

    return implode('', $reversedCharacters);
}


<?php

/**
 * This function returns a given sentence with its words
 * in reverse order
 *
 * @param string $string
 * @return string
 */
function reverseWords(string $text)
{
    $text          = trim($text);
    $words         = explode(' ', $text);
    $reversedWords = [];

    for ($i = (count($words) - 1); $i >= 0; $i--)
    {
        $reversedWords[] = $words[$i];
    }

    return implode(' ', $reversedWords);
}


<?php

use function PHPUnit\Framework\assertEquals;
use PHPUnit\Framework\TestCase;

require_once __DIR__ . '/../../vendor/autoload.php';
require_once __DIR__ . '/../../Ciphers/CaesarCipher.php';
require_once __DIR__ . '/../../Ciphers/XORCipher.php';

class CiphersTest extends TestCase
{
    public function testCaesarCipher()
    {
        $this->assertEquals('Aopz pz h alza.', encrypt('This is a test.', 7));
        $this->assertEquals('Aopz pz h alza.', encrypt('This is a test.', 7 + 26));
        $this->assertEquals('This is a test.', decrypt('Aopz pz h alza.', 7));
        $this->assertEquals('This is a test.', decrypt('Aopz pz h alza.', 7 + 26));
    }

    public function testXorCipher()
    {
        $input_str = "test@string";
        $key = "test-key";
        $invalid_key = "wrong-key";
        $this->assertEquals( $input_str, xorCipher( xorCipher( $input_str , $key) , $key));
        $this->assertNotEquals( $input_str, xorCipher( xorCipher( $input_str , $key) , $invalid_key));
    }
}


<?php
use PHPUnit\Framework\TestCase;
use function PHPUnit\Framework\assertEquals;

require_once __DIR__ . '/../../vendor/autoload.php';
require_once __DIR__ . '/../../Ciphers/MonoAlphabeticCipher.php';

class MonoAlphabeticCipherTest extends TestCase
{
    public function testMonoAlphabeticCipher(){
        $alphabet = "abcdefghijklmnopqrstuvwxyz";
        $key = "yhkqgvxfoluapwmtzecjdbsnri";
        $text = "I love1234 GitHub";
        $encryptedText = "O ambg XojFdh";

        assertEquals(maEncrypt($key, $alphabet, $text), $encryptedText);

        assertEquals(maDecrypt($key, $alphabet, $encryptedText), "I love GitHub");

    }
}

?>


<?php

use function PHPUnit\Framework\assertEquals;
use PHPUnit\Framework\TestCase;

require_once __DIR__ . '/../../vendor/autoload.php';
require_once __DIR__ . '/../../Ciphers/MorseCode.php';

class MorseCodeTest extends TestCase
{
    public function testMorseCodeCipher()
    {
        assertEquals('TEST 123', decode(encode('TEST 123')));
    }
}


<?php

use function PHPUnit\Framework\assertEquals;
use PHPUnit\Framework\TestCase;

require_once __DIR__ . '/../../vendor/autoload.php';

require_once __DIR__ . '/../../Conversions/BinaryToDecimal.php';
require_once __DIR__ . '/../../Conversions/DecimalToBinary.php';
require_once __DIR__ . '/../../Conversions/OctalToDecimal.php';
require_once __DIR__ . '/../../Conversions/HexadecimalToDecimal.php';
require_once __DIR__ . '/../../Conversions/SpeedConversion.php';

class ConversionsTest extends TestCase
{
    public function testBinaryToDecimal()
    {
        assertEquals(binaryToDecimal(111), 7);
        assertEquals(binaryToDecimal(101), 5);
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage('Please pass a valid Binary Number for Converting it to a Decimal Number.');
        binaryToDecimal("this is a string");
    }

    public function testDecimalToBinary()
    {
        assertEquals(decimalToBinary(7), 111);
        assertEquals(decimalToBinary(5), 101);
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage('Please pass a valid Decimal Number for Converting it to a Binary Number.');
        decimalToBinary("this is a string");
    }

    public function testOctalToDecimal()
    {
        assertEquals(octalToDecimal(10), 8);
        assertEquals(octalToDecimal(11), 9);
        assertEquals(octalToDecimal(1115), 589);
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage('Please pass a valid Octal Number for Converting it to a Decimal Number.');
        octalToDecimal("this is a string");
    }

    public function testDecimalToOctal()
    {
        assertEquals(decimalToOctal(8), 10);
        assertEquals(decimalToOctal(9), 11);
        assertEquals(decimalToOctal(589), 1115);
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage('Please pass a valid Decimal Number for Converting it to an Octal Number.');
        decimalToOctal("this is a string");
    }

    public function testDecimalToHex()
    {
        assertEquals(decimalToHex(10), 'A');
        assertEquals(decimalToHex(489201875), '1D28A0D3');
        assertEquals(decimalToHex(171), 'AB');
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage('Please pass a valid Decimal Number for Converting it to a Hexadecimal Number.');
        decimalToHex("this is a string");
    }

    public function testHexToDecimal()
    {
        assertEquals(hexToDecimal('A'), 10);
        assertEquals(hexToDecimal('1D28A0D3'), 489201875);
        assertEquals(hexToDecimal('AB'), 171);
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage('Please pass a valid Hexadecimal Number for Converting it to a Decimal Number.');
        hexToDecimal("this is a string");
    }

    public function testSpeedConversion()
    {
        assertEquals(convertSpeed(5, 'm/s', 'mph'), 11.18);
        assertEquals(convertSpeed(5, 'ft/s', 'km/h'), 5.49);
        assertEquals(convertSpeed(3, 'km/h', 'km/h'), 3);
        assertEquals(convertSpeed(7, 'kn', 'km/h'), 12.96);
        assertEquals(convertSpeed(12, 'mph', 'km/h'), 19.31);
        assertEquals(convertSpeed(0.514, 'm/s', 'kn'), 1);

        $this->expectException(\Exception::class);
        convertSpeed('1', 'km/h', 'mph');

        $this->expectException(\Exception::class);
        convertSpeed(1, 'km/h', 'miles');
    }
}