> $m ) & 1 ? 30 : 29; } } $starts[] = $day; // == end_day, used as the exclusive upper bound. self::$month_starts = $starts; } return array( self::$data, self::$month_starts ); } /** * Days since 1970-01-01 for a proleptic-Gregorian Y-M-D (no DST/timezone). * * Same day arithmetic as hijri.js utc(): Date.UTC(y, m-1, d) / 86400000. * * @param int $y Gregorian year. * @param int $m Gregorian month 1-12. * @param int $d Gregorian day 1-31. * @return int Epoch day. */ public static function epoch_day( $y, $m, $d ) { // Days from civil date, Howard Hinnant's algorithm (valid for all ints). $y -= $m <= 2 ? 1 : 0; $era = intdiv( $y >= 0 ? $y : $y - 399, 400 ); $yoe = $y - $era * 400; // [0, 399] $doy = intdiv( 153 * ( $m + ( $m > 2 ? -3 : 9 ) ) + 2, 5 ) + $d - 1; // [0, 365] $doe = $yoe * 365 + intdiv( $yoe, 4 ) - intdiv( $yoe, 100 ) + $doy; // [0, 146096] return $era * 146097 + $doe - 719468; } /** * Proleptic-Gregorian Y-M-D from days since 1970-01-01. * * @param int $day Epoch day. * @return array{y:int,m:int,d:int} */ public static function from_epoch_day( $day ) { $day += 719468; $era = intdiv( $day >= 0 ? $day : $day - 146096, 146097 ); $doe = $day - $era * 146097; // [0, 146096] $yoe = intdiv( $doe - intdiv( $doe, 1460 ) + intdiv( $doe, 36524 ) - intdiv( $doe, 146096 ), 365 ); $y = $yoe + $era * 400; $doy = $doe - ( 365 * $yoe + intdiv( $yoe, 4 ) - intdiv( $yoe, 100 ) ); $mp = intdiv( 5 * $doy + 2, 153 ); $d = $doy - intdiv( 153 * $mp + 2, 5 ) + 1; $m = $mp + ( $mp < 10 ? 3 : -9 ); return array( 'y' => $y + ( $m <= 2 ? 1 : 0 ), 'm' => $m, 'd' => $d, ); } /** * Gregorian -> Hijri (Umm al-Qura). Port of hijri.js toHijri(). * * @param int $gy Gregorian year. * @param int $gm Gregorian month 1-12. * @param int $gd Gregorian day. * @return array{y:int,m:int,d:int}|false Hijri date, or false outside 1300-1600 AH. */ public static function to_hijri( $gy, $gm, $gd ) { list( $data, $starts ) = self::table(); $day = self::epoch_day( $gy, $gm, $gd ); if ( $day < $data['start_day'] || $day >= $data['end_day'] ) { return false; } // Binary search: greatest i with starts[i] <= day. $lo = 0; $hi = count( $starts ) - 1; while ( $lo + 1 < $hi ) { $mid = intdiv( $lo + $hi, 2 ); if ( $starts[ $mid ] <= $day ) { $lo = $mid; } else { $hi = $mid; } } return array( 'y' => $data['start_year'] + intdiv( $lo, 12 ), 'm' => ( $lo % 12 ) + 1, 'd' => $day - $starts[ $lo ] + 1, ); } /** * Hijri (Umm al-Qura) -> Gregorian. Port of hijri.js toGregorian(). * * @param int $hy Hijri year. * @param int $hm Hijri month 1-12. * @param int $hd Hijri day 1-30. * @return array{y:int,m:int,d:int}|false Gregorian date, or false when the * date does not exist (e.g. day 30 of * a 29-day month) or is out of range. */ public static function to_gregorian( $hy, $hm, $hd ) { list( $data, $starts ) = self::table(); if ( $hy < $data['start_year'] || $hy > $data['end_year'] || $hm < 1 || $hm > 12 || $hd < 1 ) { return false; } $i = ( $hy - $data['start_year'] ) * 12 + ( $hm - 1 ); $len = $starts[ $i + 1 ] - $starts[ $i ]; if ( $hd > $len ) { return false; // تاريخ غير موجود (مثلاً يوم 30 بشهر 29 يوم) — same contract as hijri.js. } return self::from_epoch_day( $starts[ $i ] + $hd - 1 ); } /** * Number of days in a Hijri month. * * @param int $hy Hijri year. * @param int $hm Hijri month 1-12. * @return int|false 29 or 30, false out of range. */ public static function month_length( $hy, $hm ) { list( $data, $starts ) = self::table(); if ( $hy < $data['start_year'] || $hy > $data['end_year'] || $hm < 1 || $hm > 12 ) { return false; } $i = ( $hy - $data['start_year'] ) * 12 + ( $hm - 1 ); return $starts[ $i + 1 ] - $starts[ $i ]; } /** * Today's Hijri date in a given timezone. Port of Hijri.toHijri(Hijri.today()). * * @param DateTimeZone|null $tz Timezone (site timezone in WP); UTC when null. * @param int|null $now Unix timestamp, defaults to now. * @return array{y:int,m:int,d:int,wd:int}|false Hijri date plus weekday 0=Sunday. */ public static function today( $tz = null, $now = null ) { $ts = null === $now ? time() : (int) $now; $dt = new DateTime( '@' . $ts ); if ( $tz instanceof DateTimeZone ) { $dt->setTimezone( $tz ); } $h = self::to_hijri( (int) $dt->format( 'Y' ), (int) $dt->format( 'n' ), (int) $dt->format( 'j' ) ); if ( false === $h ) { return false; } $h['wd'] = (int) $dt->format( 'w' ); // 0 = Sunday, matches self::$days. return $h; } /** * "14 ربيع الآخر 1448 هـ" — port of hijri.js formatHijri(). * * @param array $h Hijri array {y,m,d}. * @return string */ public static function format_hijri( $h ) { return $h['d'] . ' ' . self::$months[ $h['m'] - 1 ] . ' ' . $h['y'] . ' هـ'; } }