13 Fechas y horas

Antes de hablar de los controles para capturar fechas y horas tenemos que conocer las clases que wxWidgets nos proporciona para tratar con estos tipos de datos.

Clases para trabajar con fechas y horas

Las clases que se usa en wxWidgets para almacenar fechas y horas son muy potentes, y nos proporcionan algunas características muy interesantes. Por ejemplo:

  • Permite almacenar un rango de fechas muy amplio, probablemente mucho más amplio de lo que seguramente necesitaremos en la mayor parte de nuestros programas. Concretamente permite fechas desde el año 4714 AC hasta más de 280 millones de años DC. Podemos, por lo tanto, olvidarnos de efectos raros derivados de los tipos para almacenar fechas.
  • Para trabajar con fechas y horas no se usan nunca cálculos en coma flotante, lo que nos asegura que la precisión siempre se mantiene.
  • Hablando de cálculos, disponemos de una variedad enorme de posibles cálculos: aritméticos, que permiten sumar y restar intervalos de tiempo; días de la semana y del año, festivos, conversiones a y desde cadenas, etc.

En este capítulo explicaremos lo más básico de las clases wxDateTime, wxDateSpan, wxTimeSpan, y otras clases auxiliares, como wxDateTimeHolidayAuthority y wxDateTimeWorkDays.

Clase wxDateTime

Para almacenar una fecha, wxWidgets usa la clase wxDateTime, que utiliza para almacenar tiempos un entero con signo de 64 bits, con una resolución de un milisegundo. Esto nos da capacidad para almacenar fechas correspondientes a casi 585 millones de años, donde el valor cero corresponde al 1 de enero de 1970.

El valor de la fecha es siempre el tiempo GMT. Las diferentes horas se calculan en función de la zona horaria definida, el país, etc.

Asumiendo que no se hagan modificaciones en el calendario Gregoriano, que es el que usamos en occidente, podemos almacenar fechas hasta más de 280 millones de años en el futuro. Sin embargo, con las fechas anteriores a 1970 existen ciertas limitaciones, provocadas por el abandono del anterior calendario Juliano. El resultado es que esta clase sólo admite fechas posteriores al 24 de noviembre de 4714 AC.

Además, debido a que diferentes países adoptaron el calendario Gregoriano en diferentes momentos, ciertos valores corresponden a fechas diferentes dependiendo del país.

Por ejemplo, William Shakespeare falleció un 23 de abril de 1616, según el calendario Juliano, vigente en Inglaterra en ese momento, y un 3 de mayo de 1616, según el calendario Gregoriano, que ya se usaba en otras partes de Europa. Miguel de Cervantes falleció el 22 de abril de 1616, pero en España ya se usaba el calendario Gregoriano, de modo que en realidad, a pesar de que se suele decir lo contrario, no fallecieron con un día de diferencia.

A partir de ese valor de 64 bits se calculan las fechas y horas usando los métodos definidos en esta clase.

Hasta la llegada de los compiladores y sistemas operativos de 64 bits, en C y C++ se ha estado usando el tiempo Unix, que usa un entero de 32 bits para almacenar fechas, que incluso con una resolución de un segundo, y con el cero en el 1 de enero de 1970. Esto sólo permite almacenar fechas hasta el 19 de enero de 2038. Espero que esto te parece un buen motivo para dejar de usar tipos como time_t en favor de tipos de 64 bits.

Afortunadamente, con la llegada de sistemas de 64 bits, ahora es posible elegir el tipo base que se usa para almacenar time_t, y lo recomendable, al menos si se pretende que el software que trabaje con fechas sobreviva después de 2038, es usar un tipo long long de 64 bits.

La clase wxDateTime hace todos los cálculos por nosotros. Todos los que hemos trabajado con fechas sabemos el dolor de cabeza que tiene hacerlo, no sólo por los cambios de calendario y los días que nunca existieron, sino también por las reglas para definir los años bisiestos, los cambios de horario de verano e invierno, las diferencias entre países, los husos horarios, etc.

Actualmente sólo se soporta el calendario Gregoriano, pero es probable que en versiones futuras se soporten otros que actualmente siguen en uso, como el hebreo, el chino o incluso el maya.

No es mi intención explicar en detalle qué puede hacer la clase wxDateTime, para ello es mejor que consultes la documentación sobre esa clase y todos los métodos que proporciona.

Tan sólo indicar que existen métodos para obtener el año, siglo, mes, día, día de la semana, día del año, hora, minuto, segundo y milisegundo. El día de la semana y el mes se pueden obtener en forma numérica o de cadena.

También se puede obtener el valor correspondiente al último día del mes para una fecha determinada, mediante GetLastMonthDay, o la del último día de la semana, con GetLastWeekDay.

Otros métodos como IsBetween, IsStrictlyBetween, IsEarlierThan, IsEqualTo, IsEqualUpTo, IsLaterThan, IsSameDate, IsSameTime, permiten comparar fechas.

Para saber si un año es bisiesto tenemos el método IsLeapYear.

Obtener un objeto con la fecha actual es fácil, usando Today o Now.

El cálculo de ciertas fechas depende del país. Para cambiar el país que se usará para realizar los cálculos se usa el método SetCountry.

Para convertir un tiempo a diferentes husos horarios disponemos de varios métodos: FromTimezone, MakeFromTimezone, MakeTimezone, MakeUTC, ToTimezone, ToUTC.

También existe una clase miembro de wxDateTime para manejar husos horarios: wxDateTime::TimeZone, y un enumerado wxDateTime::TZ, que se usa como parámetro en multitud de métodos.

Para convertir fechas y horas a cadena, en diferentes formas, disponemos de todos los métodos que empiezan con Format, como Format, FormatDate, FormatISOCombined, FormatISODate, FormatISOTime o FormatTime.

Para las conversiones desde cadena disponemos de los métodos que empiezan con Parse, como ParseFormat, ParseDateTime, ParseDate, ParseTime, ParseRfc822Date...

Aritmética de tiempos

Hay varias formas de realizar operaciones con tiempos:

  • Calcular el tiempo transcurrido o que debe transcurrir entre dos momentos diferentes. Esto no es otra cosa que restar dos fechas, y para ello disponemos del método DiffAsDateSpan.
  • Calcular fechas añadiendo o restando periodos de tiempo, para lo que disponemos de varios métodos, como Add, para añadir diferentes intervalos de tiempo o Subtract, pera restarlos. Ambos están sobrecargados para añadir o restar diferentes tipos de intervalos de tiempo. También disponemos de operadores de suma, resta para las mismas operacioens.

Tanto si usamos los métodos como los operadores, se pueden usar diferentes tipos de objetos a la hora de añadir o restar tiempos.

Estos objetos pertenecerán a dos clases adicionales: wxDateSpan y wxTimeSpan.

Clase wxDateSpan

La clase wxDateSpan se usa para especificar tiempos en el rango de años, meses, semanas y días. Se puede usar para obtener fechas como: mañana, un día más, la semana pasada, dentro de dos meses, o en un año, menos un mes y una semana, etc.

El constructor wxDateSpan permite especificar un periodo de tiempo, indicando los años, meses, semanas y días. Ninguno de los parámetros está limitado en principio, por ejemplo, se puede especificar un año y 13 meses, o dos meses, seis semanas y ocho días. También se puede usar para obtener la diferencia entre dos fechas.

Tan sólo hay que tener en cuenta que a la hora de realizar los cálculos, cada componente se trata de forma independiente. Por ejemplo, sumar un mes no añadirá los mismos días si se suma al mes de febrero que a un mes con treinta y un días, como agosto. Por ejemplo, si sumamos un mes al 31 de agosto, obtendremos la fecha 30 de septiembre, ya que septiembre sólo tiene treinta días. Sin embargo, si a continuación restamos un mes a la fecha obtenida, al 30 de septiembre, obtendremos el 30 de agosto. Esto es lo que cabría esperar, del mismo modo, al sumar un año al 29 de febrero de 2024 obtendremos la fecha del 28 de febrero de 2025, ya que 2024 es bisiesto, y en 2025 el 29 de febrero no existe.

Con las semanas y días no existe ese comportamiento, podemos sumar dos semanas y 14 días, y el resultado es el mismo que sumar 28 días o 4 semanas.

En cuanto a métodos, esta clase permite asignar cada dato miembro por separado: SetDays, SetMonths, SetWeeks o SetYears, o recuperar miembros individuales: GetDays, GetMonths, GetWeeks o GetYears. Además se pueden recuperar el total de días con GetTotalDays, que obtendrá la suma de días correspondientes a los miembros de días y semanas, y el total de meses con GetTotalMonths, que obtendrá la suma de meses correspondientes a los miembros de meses y años.

También disponemos de métodos para crear objetos con un único día, Day; con varios días, Days; con un mes, Month; con varios meses Months; con una o varias semanas, Week y Weeks, o con uno o varios años, Year y Years.

Esta clase también admite operaciones aritméticas. Dispone de métodos para sumar objetos wxDateSpan, Add, para restarlos Subtract, para cambiar el signo Neg, que se aplica directamente al objeto y Negate que crea un nuevo objeto con el signo contrario. El método sobrecargado Multiply que permite multiplicar el valor del intervalo por un entero.

Existen además operadores para algunas operaciones, operator!=, operator==, operator+=, operator-= y operator- (unitario).

    date.SetToCurrent();
    cad = date.Format(_T("Hoy: %A %d de %B de %Y"));

    date.Add(span.Day());
    cad = date.Format(_T("Mañana: %A %d de %B de %Y"));

    date.SetToCurrent();
    date.Add(span.Months(5));
    cad = date.Format(_T("En 5 meses: %A %d de %B de %Y"));

    date.SetToCurrent();
    date.Subtract(span.Years(60));
    cad = date.Format(_T("Hace 60 años: %A %d de %B de %Y"));

    date.SetToCurrent();
    date -= wxDateSpan(570, 5, 0, 3);
    cad = date.Format(_T("Hace 570 años, 5 meses y 3 días: %A %d de %B de %Y"));

Clase wxTimeSpan

La clase wxTimeSpan se usa para especificar tiempos en el rango de horas, minutos, segundos y milisegundos. Se puede usar para obtener fechas sumando o restando intervalos en el rango de días, horas, minutos, segundos y milisegundos.

Internamente se almacena el intervalo en un entero de 64 bits, y todo se almacena en milisegundos, como además no hay intervalos con rangos variables por debajo de los meses, como pasa con los meses y años en wxDateSpan, también es posible trabajar con días y semanas, ya que todos los valores por debajo de meses son fácilmente calculables.

La interfaz es parecida a la de wxDateSpan, el constructor permite especificar un valor para cada dato miembro: horas, minutos, segundos y milisegundos.

De modo que también disponemos de métodos para crear objetos a partir de una o varias semanas, Week y Weeks, uno o varios días, Day y Days, una o varias horas, Hour y Hours, uno o varios segundos, Second y Seconds, uno o varios minutos, Minute y Minutes o uno o varios milisegundos Millisecond y Milliseconds.

Se pueden obtener elementos individuales mediante GetWeeks, GetHours, GetHours, GetMinutes, GetSeconds y GetMilliseconds. O podemos obtener el valor interno en un entero de 64 bits mediante GetValue.

Dispone de métodos para comparar objetos, como IsEqualTo, IsLongerThan, IsNegative, IsNull, IsPositive, IsShorterThan.

Y también permite operaciones aritméticas como Abs, Add, Multiply, Neg, Negate y Subtract, con un comportamiento similar a sus equivalentes en wxDateSpan.

Por supuesto, también existen los operadores para algunas operaciones, wxDateSpan, operator-=, operator*= y operator- (unitario).

Adicionalmente, esta clase dispone de un método Format.

    date.SetToCurrent();
    date.Add(timespan.Hours(12));
    cad = date.Format(_T("En 12 horas: %A %d de %B de %Y %H:%M:%S"));

    date.SetToCurrent();
    date.Add(timespan.Seconds(200000));
    cad = date.Format(_T("En 200000 segundos: %A %d de %B de %Y %H:%M:%S"));

Festividades y días laborables

Disponemos de una clase auxiliar wxDateTimeHolidayAuthority que se puede usar para crear clases derivadas que definen qué días son festivos. En la versión 3.2.4, que es la última versión estable cuando escribo esto, wxWidgets sólo dispone de una clase derivada definida: wxDateTimeWorkDays, que establece como festivos los sábados y domingos. En siguientes versiones existen otras clases como wxDateTimeChristianHolidays o wxDateTimeUSCatholicFeasts, pero no hablaremos de ellas de momento.

Para establecer qué clase o clases se usarán para determinar qué días son festivos hay que añadir una instancia de esas clases usando el método AddAuthority.

   wxDateTimeHolidayAuthority::AddAuthority(new wxDateTimeWorkDays);
...

Una vez hayamos añadido las clases de autoridades de festivos que consideremos necesarias, podemos usar el método IsWorkDay de wxDateTime para saber si una fecha es festiva o no.

Si decidimos crear nuestras propias clases derivadas para definir los días festivos tendremos que declararlas como derivadas de wxDateTimeHolidayAuthority y redeclarar y definir los métodos DoIsHoliday y DoGetHolidaysInRange.

La forma más simple es, probablemente, utilizar un conjunto para definir los días festivos, e incluso definir algún método más para leer los valores desde una base de datos o fichero externo. Para este ejemplo nos limitaremos a un conjunto con sus elementos añadidos explícitamente:

#include <set>
...
class wxDateTimeFestivos2024 : public wxDateTimeHolidayAuthority
{
protected:
    virtual bool DoIsHoliday(const wxDateTime& dt) const wxOVERRIDE;
    virtual size_t DoGetHolidaysInRange(const wxDateTime& dtStart,
                                        const wxDateTime& dtEnd,
                                        wxDateTimeArray& holidays) const wxOVERRIDE;
private:
    std::set<wxDateTime> festivo = {
        {1, wxDateTime::Jan, 2024},
        {6, wxDateTime::Jan, 2024},
        {28, wxDateTime::Mar, 2024},
        {29, wxDateTime::Mar, 2024},
        {1, wxDateTime::May, 2024},
        {9, wxDateTime::May, 2024},
        {17, wxDateTime::May, 2024},
        {25, wxDateTime::Jul, 2024},
        {15, wxDateTime::Aug, 2024},
        {16, wxDateTime::Aug, 2024},
        {12, wxDateTime::Oct, 2024},
        {1, wxDateTime::Nov, 2024},
        {6, wxDateTime::Dec, 2024},
        {25, wxDateTime::Dec, 2024}
    };
};

Para la definición de los métodos, hay que tener presente que el método DoIsHoliday debe retornar true si la fecha pasada como parámetro es un día festivo. El método DoGetHolidaysInRange devuelve la cantidad de días festivos en el rango definido por los dos primeros parámetros, el tercer parámetro se utiliza para retornar un array con las fechas en ese rango:

bool wxDateTimeFestivos2024::DoIsHoliday(const wxDateTime& dt) const
{
    auto busca = festivo.find(dt);
    return busca != festivo.end();
}

size_t wxDateTimeFestivos2024::DoGetHolidaysInRange(const wxDateTime& dtStart,
                                                const wxDateTime& dtEnd,
                                                wxDateTimeArray& holidays) const
{
    if ( dtStart > dtEnd ) {
        wxFAIL_MSG( wxT("rango de fechas inválido en GetHolidaysInRange") );
        return 0u;
    }

    holidays.Empty();

    for (wxDateTime dt : festivo) {
       if(dt >= dtStart && dt <= dtEnd) holidays.Add(dt);
    }

    return holidays.GetCount();
}

Nuestro programa debe añadir esta clase al grupo de 'autoridades', de modo que posteriormente podamos determinar si una fecha concreta es o no festiva:

    wxDateTimeHolidayAuthority::AddAuthority(new wxDateTimeFestivos2024);

    date.Set(25,wxDateTime::Dec,2024,0,0,0,0);
    cad = date.Format(_T("Navidad 2024: %A %d de %B de %Y"));
...
    if(date.IsWorkDay()) cad = "Laborable"; else cad = "Festivo";
...

Ejemplo 13

Para este ejemplo usaremos algunos métodos de clases de la interfaz de gráficos para mostrar texto en la ventana. Veremos estas clases con más detalle en capítulos posteriores. Para añadir texto al área de cliente de la ventana deberemos procesar el evento EVT_PAINT usando un método de nuestra clase de ventana:

BEGIN_EVENT_TABLE(wx013Frame, wxFrame)
...
    EVT_PAINT(wx013Frame::OnPaint)
...
END_EVENT_TABLE()

Ese método recibirá un parámetro de la clase wxPaintEvent, que de momento no usaremos. En el código obtendremos un contexto de dispositivo, o DC, que funciona como una interfaz entre el programa y el dispositivo en el que queremos dibujar o mostrar texto. Ese dispositivo puede ser un monitor, una impresora, un mapa de bits, etc. De este modo es posible abstraer todas las funciones gráficas del dispositivo físico. En este caso, el DC es un wxPaintDC, que actúa de interfaz entre el programa y una ventana.

void wx013Frame::OnPaint(wxPaintEvent& event)
{
    wxPaintDC dc(this);
    wxString cad;
...
    dc.DrawText(cad, wxPoint(10,10));
...

También usaremos la clase wxLocale para que el formato de fechas se ajuste al idioma establecido por el sistema operativo, en lugar del idioma por defecto, que es inglés. También veremos este tema en un capítulo posterior, ya que wxWidgets dispone de clases para el soporte multilenguaje que pueden ser muy interesantes.

Establecer el idioma por defecto para usar el del sistema operativo es sencillo. Bastará con declarar un objeto de la clase wxLocale en nuestro objeto derivado de wxApp, e inicializarlo en el método OnInit:

...
class wx013App : public wxApp
{
    public:
        virtual bool OnInit();
    private:
        wxLocale local;
};
...
bool wx013App::OnInit()
{
    wx013Frame* frame = new wx013Frame(0L, _("wxWidgets Ejemplo 13"));
    local.Init(wxLANGUAGE_DEFAULT);
    frame->SetIcon(wxICON(aaaa)); // To Set App Icon
    frame->Show();

    return true;
}

De este modo, cuando usemos el método Format para mostrar fechas, las cadenas correspondientes a los nombres de meses y días de semana se mostrarán en el idioma definido en el sistema operativo.

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 13 wx013.zip 2024-12-25 4242 bytes 9