Tengo dos LocalDates declarados de la siguiente manera:
val startDate = LocalDate.of(2019, 10, 31) // 2019-10-31
val endDate = LocalDate.of(2019, 9, 30) // 2019-09-30
Luego calculo el período entre ellos usando Period.between función:
val period = Period.between(startDate, endDate) // P-1M-1D
Aquí el período tiene la cantidad negativa de meses y días, que se espera dado que endDate es anterior a startDate.
Sin embargo, cuando agrego eso de periodnuevo al startDate, el resultado que obtengo no es el endDate, sino la fecha un día antes:
val endDate1 = startDate.plus(period) // 2019-09-29
Entonces la pregunta es, ¿por qué no lo invariante
startDate.plus(Period.between(startDate, endDate)) == endDate
espera para estas dos fechas?
¿Es Period.betweenquién devuelve un período incorrecto o LocalDate.plusquién lo agrega incorrectamente?

date.plus(period).minus(period)) el resultado no siempre es la misma fecha. Esta pregunta es más sobrePeriod.betweenlos invariantes de la función.java.timefunciona la aritmética de calendario. Básicamente, agregar y eliminar no son intercambiables entre sí, especialmente si el día del mes de una o ambas fechas es mayor que 28. Consulte también la documentación de la clase de AbstractDuration en mi tiempo lib Time4J para obtener más antecedentes matemáticos ...AbstractDurationdocumentos afirman que la invarianciat1.plus(t1.until(t2)).equals(t2) == truedebería mantenerse, y les pregunto por qué no es el casojava.timeaquí.Respuestas:
Si observa cómo
plusse implementa paraLocalDateya veras
plusMonths(...)yplusDays(...)ahi.plusMonthsmaneja casos cuando un mes tiene 31 días, y el otro tiene 30. Por lo tanto, el siguiente código se imprimirá en2019-09-30lugar de inexistente2019-09-31Después de eso, restando un día resulta en
2019-09-29. Este es el resultado correcto, ya que2019-09-29y2019-10-31son de 1 mes 1 día de diferenciaEl
Period.betweencálculo es extraño y en este caso se reduce adonde
getProlepticMonthes el número total de meses desde 00-00-00. En este caso, es 1 mes y 1 día.Desde mi entender, es un error en una
Period.betweenyLocalDate#pluspara la interacción períodos negativo, ya que el código siguiente tiene el mismo significadopero imprime el correcto
2019-10-31.El problema es que las
LocalDate#plusMonthsfechas normalizadas son siempre "correctas". En el siguiente código, puede ver que después de restar 1 mes del2019-10-31resultado, se2019-09-31normaliza a2019-10-30fuente
Creo que simplemente no tienes suerte. El invariante que has inventado suena razonable, pero no se mantiene en java.time.
Parece que el
betweenmétodo solo resta los números de mes y los días del mes y, dado que los resultados tienen el mismo signo, se contenta con este resultado. Creo que estoy de acuerdo en que probablemente se podría haber tomado una mejor decisión aquí, pero como @Meno Hochschild ha dicho correctamente, las matemáticas que involucran los 29, 30 o 31 meses no pueden ser claras, y no me atrevo a sugerir cuál sería la mejor regla estado.Apuesto a que no lo van a cambiar ahora. Ni siquiera si presenta un informe de error (que siempre puede intentar). Demasiado código ya depende de cómo ha estado funcionando durante más de cinco años y medio.
Agregar
P-1M-1Dnuevamente a la fecha de inicio funciona de la manera que hubiera esperado. Restando 1 mes de (realmente sumando –1 mes a) el 31 de octubre se inicia el 30 de septiembre, y restando 1 día se obtiene el 29 de septiembre. Una vez más, no está claro, podría argumentar a favor del 30 de septiembre.fuente
Analizando sus expectativas (en pseudocódigo)
Tenemos que discutir varios temas:
Primero veamos las unidades. Los días no son un problema porque son la unidad de calendario más pequeña posible y cada fecha de calendario difiere de cualquier otra fecha en enteros enteros de días. Entonces siempre tenemos un pseudocódigo igual si es positivo o negativo:
Sin embargo, los meses son difíciles porque el calendario gregoriano define los meses calendario con diferentes duraciones. Entonces, puede surgir la situación de que la adición de cualquier número entero de meses a una fecha puede causar una fecha no válida:
[2019-08-31] + P1M = [2019-09-31]
La decisión de
java.timereducir la fecha de finalización a una válida, aquí [2019-09-30], es razonable y corresponde a las expectativas de la mayoría de los usuarios porque la fecha final aún conserva el mes calculado. Sin embargo, esta adición que incluye una corrección de fin de mes NO es reversible , vea la operación revertida llamada resta:[2019-09-30] - P1M = [2019-08-30]
El resultado también es razonable porque a) la regla básica de la suma mensual es mantener el día del mes lo más posible yb) [2019-08-30] + P1M = [2019-09-30].
¿Cuál es la adición de una duración (período) exactamente?
En
java.time, aPeriodes una composición de elementos que consta de años, meses y días con cualquier cantidad parcial entera. Por lo tanto, la adición de aPeriodse puede resolver a la suma de los importes parciales a la fecha de inicio. Como los años son siempre convertibles a 12 múltiplos de meses, primero podemos combinar años y meses y luego sumar el total en un solo paso para evitar efectos secundarios extraños en los años bisiestos. Los días se pueden agregar en el último paso. Un diseño razonable como hecho enjava.time.¿Cómo determinar el derecho
Periodentre dos fechas?Primero discutamos el caso cuando la duración es positiva, lo que significa que la fecha de inicio es anterior a la fecha de finalización. Entonces siempre podemos definir la duración determinando primero la diferencia en meses y luego en días. Este pedido es importante para lograr un componente de mes porque, de lo contrario, cada duración entre dos fechas solo consistiría en días. Usando sus fechas de ejemplo:
[2019-09-30] + P1M1D = [2019-10-31]
Técnicamente, la fecha de inicio se adelanta por la diferencia calculada en meses entre el inicio y el final. Luego, el día delta como diferencia entre la fecha de inicio movida y la fecha de finalización se agrega a la fecha de inicio movida. De esta manera podemos calcular la duración como P1M1D en el ejemplo. Hasta ahora muy razonable.
¿Cómo restar una duración?
El punto más interesante en el ejemplo de adición anterior es que, por accidente, NO hay corrección de fin de mes. Sin embargo,
java.timeno puede hacer la resta inversa. Primero resta los meses y luego los días:[2019-10-31] - P1M1D = [2019-09-29]
Si, en
java.timecambio, hubiera intentado revertir los pasos de la suma anterior, entonces la elección natural habría sido restar primero los días y luego los meses . Con este orden cambiado, obtendríamos [2019-09-30]. El orden cambiado en la resta ayudaría siempre que no hubiera una corrección de fin de mes en el paso de suma correspondiente. Esto es especialmente cierto si el día del mes de cualquier fecha de inicio o finalización no es mayor que 28 (la duración mínima posible del mes). Lamentablemente,java.timeha definido otro diseño para la restaPeriodque conduce a resultados menos consistentes.¿Es la suma de una duración reversible en la resta?
Primero tenemos que entender que el orden de cambio sugerido en la resta de una duración de una fecha de calendario dada no garantiza la reversibilidad de la adición. Ejemplo de contador que tiene una corrección de fin de mes en la adición:
Cambiar el orden no es malo porque produce resultados más consistentes. Pero, ¿cómo curar las deficiencias restantes? El único camino que queda es cambiar también el cálculo de la duración. En lugar de usar P3M1D, podemos ver que la duración P2M31D funcionará en ambas direcciones:
Entonces, la idea es cambiar la normalización de la duración calculada. Esto se puede hacer observando si la suma del delta del mes calculado es reversible en un paso de resta, es decir, evita la necesidad de una corrección de fin de mes.
java.timedesafortunadamente no ofrece tal solución. No es un error, pero puede considerarse como una limitación de diseño.¿Alternativas?
He mejorado mi biblioteca de tiempo Time4J con métricas reversibles que implementan las ideas dadas anteriormente. Ver el siguiente ejemplo:
fuente