Mapa YYYY-MM-DD => true. */ public static function holidays( int $year ): array { $out = array(); foreach ( self::fixed_holidays() as $mmdd ) { $out[ sprintf( '%04d-%s', $year, $mmdd ) ] = true; } $easter = self::easter( $year ); if ( $easter ) { $out[ $easter->modify( '-2 days' )->format( 'Y-m-d' ) ] = true; // Viernes Santo. $out[ $easter->modify( '-1 days' )->format( 'Y-m-d' ) ] = true; // Sábado Santo. } $extra = Settings::sanitize_date_list( (string) Settings::get( 'rights_holidays', '' ) ); if ( '' !== $extra ) { foreach ( explode( "\n", $extra ) as $date ) { $out[ $date ] = true; } } /** * Permite agregar feriados al calendario usado para los días hábiles. * * @param array $out Feriados. * @param int $year Año. */ return (array) apply_filters( 'cumpleconsent_holidays', $out, $year ); } /** * Domingo de Pascua (algoritmo de Meeus/Jones/Butcher). * * @param int $year Año. * @return \DateTimeImmutable|null */ public static function easter( int $year ) { $a = $year % 19; $b = intdiv( $year, 100 ); $c = $year % 100; $d = intdiv( $b, 4 ); $e = $b % 4; $f = intdiv( $b + 8, 25 ); $g = intdiv( $b - $f + 1, 3 ); $h = ( 19 * $a + $b - $d - $g + 15 ) % 30; $i = intdiv( $c, 4 ); $k = $c % 4; $l = ( 32 + 2 * $e + 2 * $i - $h - $k ) % 7; $m = intdiv( $a + 11 * $h + 22 * $l, 451 ); $n = intdiv( $h + $l - 7 * $m + 114, 31 ); $p = ( $h + $l - 7 * $m + 114 ) % 31; return new \DateTimeImmutable( sprintf( '%04d-%02d-%02d', $year, $n, $p + 1 ), new \DateTimeZone( 'America/Santiago' ) ); } /** ¿Es día hábil (lunes a viernes, no feriado)? */ public static function is_working_day( \DateTimeInterface $date ): bool { $dow = (int) $date->format( 'N' ); if ( $dow > 5 ) { return false; } $holidays = self::holidays( (int) $date->format( 'Y' ) ); return ! isset( $holidays[ $date->format( 'Y-m-d' ) ] ); } /** * Suma días hábiles a una fecha (si la fecha de inicio no es hábil, cuenta * desde el siguiente día hábil). * * @param string $start Fecha Y-m-d (hora de Chile). * @param int $days Días hábiles a sumar. * @return string Fecha Y-m-d. */ public static function add_working_days( string $start, int $days ): string { $tz = new \DateTimeZone( 'America/Santiago' ); $date = new \DateTimeImmutable( $start . ' 00:00:00', $tz ); $left = max( 0, $days ); $guard = 0; while ( $left > 0 && $guard < 4000 ) { $date = $date->modify( '+1 day' ); ++$guard; if ( self::is_working_day( $date ) ) { --$left; } } return $date->format( 'Y-m-d' ); } /** * Suma días corridos a una fecha. * * @param string $start Fecha Y-m-d. * @param int $days Días corridos. * @return string Fecha Y-m-d. */ public static function add_calendar_days( string $start, int $days ): string { $tz = new \DateTimeZone( 'America/Santiago' ); $date = new \DateTimeImmutable( $start . ' 00:00:00', $tz ); return $date->modify( '+' . max( 0, $days ) . ' days' )->format( 'Y-m-d' ); } /** * Plazos de una solicitud de derechos. * * @param string $received Fecha de ingreso Y-m-d. * @param bool $is_block Si es solicitud de bloqueo temporal (2 días hábiles). * @return array Claves: due, extended, block_due. */ public static function schedule( string $received, bool $is_block = false ): array { return array( 'due' => self::add_calendar_days( $received, 30 ), 'extended' => self::add_calendar_days( $received, 60 ), 'block_due' => $is_block ? self::add_working_days( $received, 2 ) : '', ); } /** * Días restantes hasta una fecha (negativo si ya venció), en fecha de Chile. * * @param string $due Fecha Y-m-d. * @return int */ public static function days_left( string $due ): int { $tz = new \DateTimeZone( 'America/Santiago' ); $now = new \DateTimeImmutable( 'now', $tz ); $then = new \DateTimeImmutable( $due . ' 23:59:59', $tz ); $diff = $then->getTimestamp() - $now->getTimestamp(); return (int) floor( $diff / DAY_IN_SECONDS ); } /** * Etiqueta de estado del plazo para la bandeja de solicitudes. * * @param string $due Fecha Y-m-d. * @return array{state:string,label:string,days:int} */ public static function status( string $due ): array { if ( '' === $due ) { return array( 'state' => 'none', 'label' => '—', 'days' => 0, ); } $days = self::days_left( $due ); if ( $days < 0 ) { return array( 'state' => 'overdue', 'label' => sprintf( /* translators: %d: días de atraso */ _n( 'Vencido hace %d día', 'Vencido hace %d días', abs( $days ), 'cumpleconsent' ), abs( $days ) ), 'days' => $days, ); } if ( $days <= 3 ) { return array( 'state' => 'soon', 'label' => sprintf( /* translators: %d: días restantes */ _n( 'Vence en %d día', 'Vence en %d días', $days, 'cumpleconsent' ), $days ), 'days' => $days, ); } return array( 'state' => 'ok', 'label' => sprintf( /* translators: %d: días restantes */ _n( 'Quedan %d día', 'Quedan %d días', $days, 'cumpleconsent' ), $days ), 'days' => $days, ); } }