Estoy de acuerdo con el sentimiento del OP de que esto es contraintuitivo y frustrante, pero también lo es determinar qué +1 month significa en los escenarios en los que esto ocurre. Considere estos ejemplos:
Comienza con 2015-01-31 y desea agregar un mes 6 veces para obtener un ciclo de programación para enviar un boletín por correo electrónico. Con las expectativas iniciales del OP en mente, esto regresaría:
- 2015-01-31
- 2015-02-28
- 2015-03-31
- 2015-04-30
- 2015-05-31
- 2015-06-30
De inmediato, observe que estamos esperando +1 monthquerer decirlast day of month o, alternativamente, agregar 1 mes por iteración, pero siempre en referencia al punto de inicio. En lugar de interpretar esto como "último día del mes", podríamos leerlo como "día 31 del próximo mes o último disponible dentro de ese mes". Esto significa que saltamos del 30 de abril al 31 de mayo en lugar del 30 de mayo. Tenga en cuenta que esto no se debe a que sea el "último día del mes", sino a que queremos "el más cercano disponible a la fecha del mes de inicio".
Entonces, supongamos que uno de nuestros usuarios se suscribe a otro boletín para comenzar el 30-01-2015. ¿Para qué es la fecha intuitiva +1 month? Una interpretación sería "día 30 del próximo mes o el más cercano disponible" que devolvería:
- 2015-01-30
- 2015-02-28
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
Esto estaría bien, excepto cuando nuestro usuario reciba ambos boletines el mismo día. Supongamos que se trata de un problema del lado de la oferta y no del lado de la demanda.No nos preocupa que el usuario se moleste al recibir 2 boletines el mismo día, sino que nuestros servidores de correo no pueden pagar el ancho de banda para enviar el doble de muchos boletines. Con eso en mente, volvemos a la otra interpretación de "+1 mes" como "enviar el penúltimo día de cada mes", que devolvería:
- 2015-01-30
- 2015-02-27
- 2015-03-30
- 2015-04-29
- 2015-05-30
- 2015-06-29
Ahora hemos evitado cualquier superposición con el primer conjunto, pero también terminamos con el 29 de abril y junio, lo que ciertamente coincide con nuestras intuiciones originales que +1 monthsimplemente deberían regresar m/$d/Yo lo atractivo y simple m/30/Ypara todos los meses posibles. Así que ahora consideremos una tercera interpretación del +1 monthuso de ambas fechas:
31 de enero
- 2015-01-31
- 2015-03-03
- 2015-03-31
- 2015-05-01
- 2015-05-31
- 2015-07-01
30 de enero
- 2015-01-30
- 2015-03-02
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
Lo anterior tiene algunos problemas. Se omite febrero, lo que podría ser un problema tanto para el final de la oferta (por ejemplo, si hay una asignación mensual de ancho de banda y febrero se desperdicia y marzo se duplica) como el final de la demanda (los usuarios se sienten estafados en febrero y perciben el marzo adicional como intento de corregir el error). Por otro lado, observe que los dos conjuntos de fechas:
- nunca se superponga
- siempre están en la misma fecha cuando ese mes tiene la fecha (por lo que el conjunto del 30 de enero se ve bastante limpio)
- están todos dentro de los 3 días (1 día en la mayoría de los casos) de lo que podría considerarse la fecha "correcta".
- son todos al menos 28 días (un mes lunar) de su sucesor y predecesor, por lo que están distribuidos de manera muy uniforme.
Dados los dos últimos conjuntos, no sería difícil simplemente revertir una de las fechas si cae fuera del mes siguiente real (así que retroceda al 28 de febrero y al 30 de abril en el primer conjunto) y no perder el sueño durante el superposición ocasional y divergencia del patrón del "último día del mes" frente al patrón del "segundo al último día del mes". Pero esperar que la biblioteca elija entre "más bonito / natural", "interpretación matemática del 31/02 y otros desbordamientos del mes" y "relativo al primero o al último mes" siempre terminará con las expectativas de alguien que no se cumplan y algunos programas necesitan ajustar la fecha "incorrecta" para evitar el problema del mundo real que introduce la interpretación "incorrecta".
Entonces, nuevamente, aunque también esperaría +1 monthdevolver una fecha que en realidad es el mes siguiente, no es tan simple como la intuición y, dadas las opciones, ir con las matemáticas sobre las expectativas de los desarrolladores web es probablemente la opción segura.
Aquí hay una solución alternativa que sigue siendo tan torpe como cualquier otra, pero creo que tiene buenos resultados:
foreach(range(0,5) as $count) {
$new_date = clone $date;
$new_date->modify("+$count month");
$expected_month = $count + 1;
$actual_month = $new_date->format("m");
if($expected_month != $actual_month) {
$new_date = clone $date;
$new_date->modify("+". ($count - 1) . " month");
$new_date->modify("+4 weeks");
}
echo "* " . nl2br($new_date->format("Y-m-d") . PHP_EOL);
}
No es óptimo, pero la lógica subyacente es: si agregar 1 mes da como resultado una fecha diferente a la prevista para el próximo mes, elimine esa fecha y agregue 4 semanas en su lugar. Aquí están los resultados con las dos fechas de prueba:
31 de enero
- 2015-01-31
- 2015-02-28
- 2015-03-31
- 2015-04-28
- 2015-05-31
- 2015-06-28
30 de enero
- 2015-01-30
- 2015-02-27
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
(Mi código es un desastre y no funcionaría en un escenario de varios años. Le doy la bienvenida a cualquiera para que reescriba la solución con un código más elegante siempre que la premisa subyacente se mantenga intacta, es decir, si +1 mes devuelve una fecha original, use +4 semanas en su lugar.)