¿Por qué es enero el mes 0 en el calendario Java?

300

En java.util.Calendarenero, se define como el mes 0, no el mes 1. ¿Hay alguna razón específica para eso?

He visto a muchas personas confundirse con eso ...

Stéphane Bonniez
fuente
44
¿No es ese un detalle de implementación, ya que las constantes ENERO, FEBRERO, etc. existen? Las clases de fecha son anteriores al soporte adecuado de Java Enum.
gnud
66
Aún más molesto: ¿por qué hay un diciembre?
mate b
40
@gnud: No, no es un detalle de implementación. Es difícil cuando se le ha dado un número entero en base "natural" (es decir, Jan = 1) y necesita usarlo con la API de calendario.
Jon Skeet
1
@matt b: es para calendarios no gregorianos (calendarios lunares, etc.) que tienen trece meses. Por eso es mejor no pensar en términos de números, sino dejar que Calendar haga su localización.
erickson
77
El argumento de 13 meses no tiene sentido. Si es así, ¿por qué no hacer que el mes adicional sea 0 o 13?
Quinn Taylor el

Respuestas:

323

Es solo parte del desorden horrendo que es la API de fecha / hora de Java. Enumerar lo que está mal llevaría mucho tiempo (y estoy seguro de que no sé la mitad de los problemas). Es cierto que trabajar con fechas y horas es complicado, pero aaargh de todos modos.

Hazte un favor y usa Joda Time en su lugar, o posiblemente JSR-310 .

EDITAR: En cuanto a las razones por las cuales, como se señaló en otras respuestas, bien podría deberse a las antiguas API de C, o simplemente a una sensación general de comenzar todo desde 0 ... excepto que los días comienzan con 1, por supuesto. Dudo si alguien fuera del equipo de implementación original realmente podría explicar las razones, pero nuevamente, insto a los lectores a que no se preocupen tanto por las malas decisiones que se tomaron, sino que observen toda la gama de maldad java.util.Calendary encuentren algo mejor.

Un punto que está a favor de usar índices basados ​​en 0 es que facilita cosas como "matrices de nombres":

// I "know" there are 12 months
String[] monthNames = new String[12]; // and populate...
String name = monthNames[calendar.get(Calendar.MONTH)];

Por supuesto, esto falla tan pronto como obtienes un calendario con 13 meses ... pero al menos el tamaño especificado es la cantidad de meses que esperas.

Esta no es una buena razón, pero es una razón ...

EDITAR: Como comentario, solicita algunas ideas sobre lo que creo que está mal con Fecha / Calendario:

  • Bases sorprendentes (1900 como la base del año en Date, ciertamente para constructores obsoletos; 0 como la base del mes en ambos)
  • Mutabilidad: el uso de tipos inmutables hace que sea mucho más sencillo trabajar con valores realmente efectivos
  • Un conjunto de tipos insuficiente: es bueno tenerlo Datey Calendarcomo cosas diferentes, pero falta la separación de valores "locales" frente a "divididos en zonas", como es la fecha / hora frente a fecha frente a hora
  • Una API que conduce a un código feo con constantes mágicas, en lugar de métodos claramente nombrados
  • Una API que es muy difícil de razonar: todo el negocio sobre cuándo se recalculan las cosas, etc.
  • El uso de constructores sin parámetros por defecto a "ahora", lo que conduce a un código difícil de probar
  • La Date.toString()implementación que siempre usa la zona horaria local del sistema (eso ha confundido a muchos usuarios de Stack Overflow antes)
Jon Skeet
fuente
14
... y ¿qué pasa con la depreciación de todos los métodos simples de fecha útiles? Ahora tengo que usar ese horrible objeto Calendario de maneras complicadas para hacer cosas que solían ser simples.
Brian Knoblauch
3
@Brian: siento tu dolor. Una vez más, Joda Time es más simple :) (El factor de inmutabilidad hace que las cosas sean mucho más agradables para trabajar también)
Jon Skeet
8
No respondiste la pregunta.
Zeemee
2
@ user443854: He enumerado algunos puntos en una edición, vea si eso ayuda.
Jon Skeet
2
Si está utilizando Java 8, puede deshacerse de la clase Calendar y cambiar a la nueva y elegante API DateTime . La nueva API también incluye un DateTimeFormatter inmutable / threadsafe, que es una gran mejora sobre el problemático y costoso SimpleDateFormat.
ccpizza
43

Porque hacer matemáticas con meses es mucho más fácil.

1 mes después de diciembre es enero, pero para resolver esto normalmente tendrías que tomar el número del mes y hacer cálculos

12 + 1 = 13 // What month is 13?

¡Lo sé! Puedo arreglar esto rápidamente usando un módulo de 12.

(12 + 1) % 12 = 1

Esto funciona bien durante 11 meses hasta noviembre ...

(11 + 1) % 12 = 0 // What month is 0?

Puede hacer que todo esto funcione nuevamente restando 1 antes de agregar el mes, luego haga su módulo y finalmente agregue 1 nuevamente ... también conocido como evitar un problema subyacente.

((11 - 1 + 1) % 12) + 1 = 12 // Lots of magical numbers!

Ahora pensemos en el problema con los meses 0-11.

(0 + 1) % 12 = 1 // February
(1 + 1) % 12 = 2 // March
(2 + 1) % 12 = 3 // April
(3 + 1) % 12 = 4 // May
(4 + 1) % 12 = 5 // June
(5 + 1) % 12 = 6 // July
(6 + 1) % 12 = 7 // August
(7 + 1) % 12 = 8 // September
(8 + 1) % 12 = 9 // October
(9 + 1) % 12 = 10 // November
(10 + 1) % 12 = 11 // December
(11 + 1) % 12 = 0 // January

Todos los meses funcionan igual y no es necesario un trabajo alternativo.

Arucker
fuente
55
Esto es satisfactorio ¡Al menos hay algo de valor en esta locura!
moljac024
"Muchos números mágicos" - no, es solo uno que aparece dos veces.
user123444555621
Sin embargo, retroceder un mes todavía es un poco complicado, gracias al uso desafortunado de C de un operador "restante" en lugar de "módulo". Tampoco estoy seguro de la frecuencia con la que uno realmente necesita aumentar un mes sin ajustar el año, y que los meses pasen del 1 al 12 no crea ningún problema con `while (month> 12) {month- = 12; año ++;}
supercat
2
Debido a que las funciones sensatas como DateTime.AddMonths son demasiado difíciles de implementar correctamente en una biblioteca, tenemos que hacer los cálculos que usted describió ... Mmmmmkay
nsimeonov
8
No entiendo estos votos a favor: ((11 - 1 + 1) % 12) + 1 = 12es solo (11 % 12) + 1para los meses 1 ... 12, solo necesita agregar el 1 después de hacer el módulo. No se requiere magia.
mfitzp
35

Los lenguajes basados ​​en C copian C hasta cierto punto. La tmestructura (definida en time.h) tiene un campo entero tm_moncon el rango (comentado) de 0-11.

Los lenguajes basados ​​en C comienzan las matrices en el índice 0. Por lo tanto, esto era conveniente para generar una cadena en una matriz de nombres de mes, con tm_monel índice.

stesch
fuente
22

Ha habido muchas respuestas a esto, pero daré mi opinión sobre el tema de todos modos. La razón detrás de este comportamiento extraño, como se indicó anteriormente, proviene del POSIX C, time.hdonde los meses se almacenaron en un int con el rango 0-11. Para explicar por qué, míralo así; Los años y los días se consideran números en el idioma hablado, pero los meses tienen sus propios nombres. Entonces, como enero es el primer mes, se almacenará como desplazamiento 0, el primer elemento de la matriz. monthname[JANUARY]sería "January". El primer mes del año es el primer elemento de matriz del mes.

Los números de día, por otro lado, ya que no tienen nombres, almacenarlos en un int como 0-30 sería confuso, agregar muchas day+1instrucciones para generar y, por supuesto, ser propensos a muchos errores.

Dicho esto, la inconsistencia es confusa, especialmente en javascript (que también ha heredado esta "característica"), un lenguaje de secuencias de comandos donde esto debería abstraerse lejos del idioma.

TL; DR : Porque los meses tienen nombres y los días del mes no.

Piksel Bitworks
fuente
1
"los meses tienen nombres y los días no". ¿Has oído hablar del 'viernes'? ;) Bien, supongo que querías decir "..los días del mes no" - tal vez valga la pena editar tu (por lo demás buena) respuesta. :-)
Andrew Thompson
¿0/0/0000 se representa mejor como "00-Jan-0000" o como "00-XXX-0000"? En mi humilde opinión, un montón de código habría sido más limpio si hubiera trece "meses", pero el mes 0 recibió un nombre falso.
supercat
1
Esa es una toma interesante, pero 0/0/0000 no es una fecha válida. ¿Cómo representaría 40/40/0000?
piksel bitworks
12

En Java 8, hay una nueva API de fecha / hora JSR 310 que es más sensata. El líder de especificaciones es el mismo que el autor principal de JodaTime y comparten muchos conceptos y patrones similares.

Alex Miller
fuente
2
La nueva API de fecha y hora ahora es parte de Java 8
mschenk74
9

Yo diría pereza. Las matrices comienzan en 0 (todos lo saben); los meses del año son un conjunto, lo que me lleva a creer que un ingeniero de Sun simplemente no se molestó en poner esta pequeña delicadeza en el código Java.

El Pitufo
fuente
99
No, no lo haría. Es más importante optimizar la eficiencia de los clientes que los programadores. Como este cliente pasa tiempo preguntando, fallaron en eso.
TheSmurf
2
No tiene ninguna relación con la eficiencia: no es como si los meses estuvieran almacenados en una matriz y necesitaría 13 para representar 12 meses. Se trata de no hacer que la API sea tan fácil de usar como deberían haber sido en primer lugar. Trapos de Josh Bloch sobre Fecha y Calendario en "Java efectivo". Muy pocas API son perfectas, y las API de fecha / hora en Java tienen el desafortunado papel de ser las tontas. Así es la vida, pero no pretendamos que tiene algo que ver con la eficiencia.
Quinn Taylor
1
¿Por qué no contar los días del 0 al 30 entonces? Es simplemente inconsistente y descuidado.
Juangui Jordán
9

Probablemente porque la "estructura tm" de C hace lo mismo.

Paul Tomblin
fuente
8

Porque los programadores están obsesionados con los índices basados ​​en 0. OK, es un poco más complicado que eso: tiene más sentido cuando trabajas con lógica de nivel inferior para usar indexación basada en 0. Pero en general, seguiré con mi primera oración.

Dinah
fuente
1
Esta es otra de esas expresiones idiomáticas / hábitos que van camino de vuelta a ensamblador o lenguaje de máquina en el que todo se hace en términos de compensaciones, no indexa. La notación de matriz se convirtió en un atajo para acceder a bloques contiguos, comenzando en el desplazamiento 0.
Ken Gentle
4

Personalmente, tomé la extrañeza de la API del calendario Java como una indicación de que necesitaba divorciarme de la mentalidad gregoriana y tratar de programar de manera más agnóstica en ese sentido. Específicamente, aprendí una vez más a evitar constantes codificadas para cosas como meses.

¿Cuál de los siguientes es más probable que sea correcto?

if (date.getMonth() == 3) out.print("March");

if (date.getMonth() == Calendar.MARCH) out.print("March");

Esto ilustra una cosa que me molesta un poco sobre Joda Time: puede alentar a los programadores a pensar en términos de constantes codificadas. (Sin embargo, solo un poco. No es como si Joda estuviera obligando a los programadores a programar mal).

Paul Brinkley
fuente
1
Pero qué esquema es más probable que le cause dolor de cabeza cuando no tiene una constante en su código: tiene un valor que es el resultado de una llamada al servicio web o lo que sea.
Jon Skeet
Esa llamada al servicio web también debería usar esa constante, por supuesto. :-) Lo mismo ocurre con cualquier persona que llama externa. Una vez que hemos establecido que existen múltiples estándares, la necesidad de hacer cumplir uno se hace evidente. (Espero haber entendido tu comentario ...)
Paul Brinkley
3
Sí, deberíamos aplicar el estándar que casi todo lo demás en el mundo usa cuando expresa meses: el estándar basado en 1.
Jon Skeet
La palabra clave aquí es "casi". Obviamente, Jan = 1, etc. se siente natural en un sistema de fechas con un uso extremadamente amplio, pero ¿por qué permitirnos hacer una excepción para evitar constantes codificadas, incluso en este caso?
Paul Brinkley
3
Porque hace la vida más fácil. Simplemente lo hace. He nunca se encontró un problema off-by-one con un sistema basado en 1 mes. He visto muchos errores con la API de Java. Ignorar lo que hacen los demás en el mundo simplemente no tiene sentido.
Jon Skeet
4

Para mí, nadie lo explica mejor que mindpro.com :

Gotchas

java.util.GregorianCalendartiene muchos menos errores y problemas que la old java.util.Dateclase, pero todavía no es un día de campo.

Si hubiera habido programadores cuando se propuso por primera vez el horario de verano, lo habrían vetado como una locura e intratable. Con el horario de verano, existe una ambigüedad fundamental. En el otoño, cuando retrasas tus relojes una hora a las 2 AM, hay dos instantes diferentes en el tiempo, ambos llamados 1:30 AM hora local. Puede distinguirlos solo si registra si tenía previsto el horario de verano o el horario estándar con la lectura.

Desafortunadamente, no hay forma de saber GregorianCalendarcuál fue su intención. Debe recurrir a decirle la hora local con la zona horaria UTC ficticia para evitar la ambigüedad. Los programadores generalmente cierran los ojos a este problema y solo esperan que nadie haga nada durante esta hora.

Bicho del milenio. Los errores aún no están fuera de las clases de calendario. Incluso en JDK (Java Development Kit) 1.3 hay un error de 2001. Considere el siguiente código:

GregorianCalendar gc = new GregorianCalendar();
gc.setLenient( false );
/* Bug only manifests if lenient set false */
gc.set( 2001, 1, 1, 1, 0, 0 );
int year = gc.get ( Calendar.YEAR );
/* throws exception */

El error desaparece a las 7 a.m. del 01/01 2001 para MST.

GregorianCalendarestá controlado por un gigante de pila de constantes mágicas int sin tipo. Esta técnica destruye totalmente cualquier esperanza de verificación de errores en tiempo de compilación. Por ejemplo para obtener el mes que usas GregorianCalendar. get(Calendar.MONTH));

GregorianCalendartiene el crudo GregorianCalendar.get(Calendar.ZONE_OFFSET)y el horario de verano GregorianCalendar. get( Calendar. DST_OFFSET) , pero no hay forma de obtener el desplazamiento de zona horaria real que se está utilizando. Debe obtener estos dos por separado y agregarlos juntos.

GregorianCalendar.set( year, month, day, hour, minute) no establece los segundos a 0.

DateFormat y GregorianCalendar no encajan correctamente. Debe especificar el Calendario dos veces, una vez indirectamente como una Fecha.

Si el usuario no ha configurado su zona horaria correctamente, el valor predeterminado será silenciosamente PST o GMT.

En GregorianCalendar, los meses están numerados a partir de enero = 0, en lugar de 1 como todos los demás en el planeta. Sin embargo, los días comienzan en 1 al igual que los días de la semana con domingo = 1, lunes = 2, ... sábado = 7. Sin embargo, DateFormat. parse se comporta de la manera tradicional con enero = 1.

Edwin Dalorzo
fuente
4

java.util.Month

Java le proporciona otra forma de usar índices basados ​​en 1 durante meses. Usa la java.time.Monthenumeración. Un objeto está predefinido para cada uno de los doce meses. Tienen números asignados a cada 1-12 para enero-diciembre; Llame getValuepor el número.

Utiliza Month.JULY(Te da 7) en lugar de Calendar.JULY(Te da 6).

(import java.time.*;)
Digital_Reality
fuente
3

tl; dr

Month.FEBRUARY.getValue()  // February → 2.

2

Detalles

La respuesta de Jon Skeet es correcta.

Ahora tenemos un reemplazo moderno para esas problemáticas clases antiguas de fecha y hora heredadas: las clases java.time .

java.time.Month

Entre esas clases está la enumeración . Una enumeración lleva uno o más objetos predefinidos, objetos que se instancian automáticamente cuando se carga la clase. En tenemos una docena de tales objetos, cada uno dado un nombre: , , , y así sucesivamente. Cada uno de esos es una constante de clase. Puede usar y pasar estos objetos a cualquier parte de su código. Ejemplo:Month MonthJANUARYFEBRUARYMARCHstatic final publicsomeMethod( Month.AUGUST )

Afortunadamente, tienen una numeración sensata, 1-12 donde 1 es enero y 12 es diciembre.

Obtenga un Monthobjeto para un número de mes en particular (1-12).

Month month = Month.of( 2 );  // 2 → February.

Yendo en la otra dirección, solicite a un Monthobjeto su número de mes.

int monthNumber = Month.FEBRUARY.getValue();  // February → 2.

Muchos otros métodos útiles en esta clase, como saber la cantidad de días en cada mes . La clase incluso puede generar un nombre localizado del mes.

Puede obtener el nombre localizado del mes, en varias longitudes o abreviaturas.

String output = 
    Month.FEBRUARY.getDisplayName( 
        TextStyle.FULL , 
        Locale.CANADA_FRENCH 
    );

février

Además, debe pasar objetos de esta enumeración alrededor de su base de código en lugar de simples números enteros . Hacerlo proporciona seguridad de escritura, asegura un rango válido de valores y hace que su código sea más autodocumentado. Consulte el Tutorial de Oracle si no está familiarizado con la increíblemente poderosa instalación de enumeración en Java.

También puede encontrar útiles las clases Yeary YearMonth.


Sobre java.time

El marco java.time está integrado en Java 8 y versiones posteriores. Estas clases suplantar la vieja problemáticos heredados clases de fecha y hora como java.util.Date, .Calendar, y java.text.SimpleDateFormat.

El proyecto Joda-Time , ahora en modo de mantenimiento , aconseja la migración a java.time.

Para obtener más información, consulte el Tutorial de Oracle . Y busque Stack Overflow para obtener muchos ejemplos y explicaciones. La especificación es JSR 310 .

¿Dónde obtener las clases java.time?

  • Java SE 8 y SE 9 y posterior
    • Incorporado.
    • Parte de la API Java estándar con una implementación integrada.
    • Java 9 agrega algunas características menores y correcciones.
  • Java SE 6 y SE 7
    • Gran parte de la funcionalidad java.time se transfiere a Java 6 y 7 en ThreeTen-Backport .
  • Androide

El proyecto ThreeTen-Extra extiende java.time con clases adicionales. Este proyecto es un campo de pruebas para posibles adiciones futuras a java.time. Usted puede encontrar algunas clases útiles aquí, como Interval, YearWeek, YearQuarter, y más .

Albahaca Bourque
fuente
0

No se define exactamente como cero per se, se define como Calendario. Enero. Es el problema de usar ints como constantes en lugar de enumeraciones. Calendario enero == 0.

Pål GD
fuente
1
Los valores son uno y lo mismo. Las API también pueden devolver 0, es idéntico a la constante. Calendar.JANUARY podría haberse definido como 1, ese es el punto. Una enumeración sería una buena solución, pero las enumeraciones verdaderas no se agregaron al lenguaje hasta Java 5, y Date ha estado presente desde el principio. Es desafortunado, pero realmente no puede "arreglar" una API tan fundamental una vez que el código de un tercero la usa. Lo mejor que se puede hacer es proporcionar una nueva API y desaprobar la antigua para alentar a las personas a seguir adelante. Gracias, Java 7 ...
Quinn Taylor
0

Debido a que la escritura de idiomas es más difícil de lo que parece, y el tiempo de manejo en particular es mucho más difícil de lo que la mayoría de la gente piensa. Para una pequeña parte del problema (en realidad, no Java), vea el video de YouTube "El problema con el tiempo y las zonas horarias - Computerphile" en https://www.youtube.com/watch?v=-5wpm-gesOY . No se sorprenda si su cabeza se cae de la risa en la confusión.

Tihamer
fuente
-1

Además de la respuesta de holgazanería de DannySmurf, agregaré que es para alentarlo a usar las constantes, como Calendar.JANUARY.

Powerlord
fuente
55
Eso está muy bien cuando está escribiendo explícitamente el código para un mes en particular, pero es un dolor cuando tiene el mes en forma "normal" de una fuente diferente.
Jon Skeet
1
También es una molestia cuando intentas imprimir el valor de ese mes de alguna manera en particular: siempre le agregas 1.
Brian Warshaw
-2

Porque todo comienza con 0. Este es un hecho básico de programación en Java. Si una cosa se desviara de eso, eso conduciría a una gran confusión. No discutamos la formación de ellos y codifiquemos con ellos.

Syrrus
fuente
2
No, la mayoría de las cosas en el mundo real comienzan con 1. Las compensaciones comienzan con 0, y el mes del año no es un desplazamiento, es uno de los doce, al igual que el día del mes es uno de los 31 o 30 o 29 o 28. Tratar el mes como una compensación es simplemente caprichoso, especialmente si al mismo tiempo no tratamos el día del mes de la misma manera. ¿Cuál sería la razón de esta diferencia?
SantiBailors
en el mundo real, comience con 1, en el mundo de Java comience con 0. PERO ... Creo que es porque: - para calcular el día de la semana no se puede compensar por un par de cálculos sin agregar un par de pasos más para ..., además, muestra los días completos del mes si es necesario (sin confusión ni necesidad de consultar febrero). Para el mes, le obliga a imprimir en un formato de fecha que debe usarse de cualquier manera. Además, dado que el número de meses en un año es regular y los días en un mes no tienen sentido si necesita declarar matrices y usar un desplazamiento para adaptarse mejor a la matriz.
Syrrus