PROGRAMOWANIE – PRZYKŁADY
IDE C++ WinBGI C# Wpadki

Przykład C++

Kolorowanie kalendarza miesięcznego Algorytm wyznaczania daty Wielkanocy Moduł obliczeniowy Program w Borland C++ Program w MinGW C++ Program w Visual C++ Poprzedni przykład Następny przykład Program w Visual C# Kontakt

Kolorowanie kalendarza miesięcznego

Program wyświetlania kalendarza miesięcznego byłby bardziej atra­kcyjny, gdyby wyróżniał dni świąte­czne i datę bieżącą innym kolorem, np. czerwonym i fiole­towym. Oczywiście niedziele i święta stałe nie nastrę­czają kłopotu, problemem jest określenie dat świąt ruchomych – Wielka­nocy (właściwie Ponie­działku Wielka­nocnego) i Bożego Ciała.

Zestaw świąt religijnych i państwowych w Polsce zmieniał się co jakiś czas, toteż w dalszych rozważa­niach przyjmiemy ich aktualny wykaz. Daty świąt przypada­jących niekonie­cznie w niedziele i wyróżnia­nych w kalen­darzach jak niedziele można zapisać w języku C++ w tablicy złożonej z 11 ele­mentów, z których każdy jest strukturą składa­jącą się z dwóch pól zainicjali­zowanych numerem miesiąca i numerem dnia:

struct dz_sw
{
    int m, d;       // miesiąc, dzień
} Rd[] =
{
    {1, 1},         // Nowy Rok
    {1, 6},         // Trzech Króli (od 2011 r.)
    {0, 0},         // Poniedziałek Wielkanocny
    {5, 1},         // Święto Pracy
    {5, 3},         // Święto Konstytucji 3 Maja
    {0, 0},         // Boże Ciało
    {8, 15},        // Wniebowzięcie NMP
    {11, 1},        // Wszystkich Świętych
    {11, 11},       // Święto Niepodległości
    {12, 25},       // Boże Narodzenie (1 dzień)
    {12, 26}        // Boże Narodzenie (2 dzień)
};

Wartości dwóch elementów tablicy Rd, o inde­ksach 2 (Ponie­działek Wielka­nocny) i 5 (Boże Ciało), są zależne od numeru roku i wyma­gają wyzna­czenia. Warto zwrócić uwagę na umieszczenie tablicy bezpo­średnio w defi­nicji struktury dz_sw, tj. pomiędzy nawiasem klamrowym zamykającym listę pól a średnikiem kończącym definicję.

Algorytm wyznaczania daty Wielkanocy

Wielkanoc przypada w pierwszą niedzielę po pierwszej pełni Księżyca po równo­nocy wiosennej, Ponie­działek Wielka­nocny jest pierwszym dniem po Wielka­nocy, zaś Boże Ciało sześćdzie­siątym. Problem uzupeł­nienia dwóch elementów tablicy Rd spro­wadza się więc do obli­czenia daty Wielka­nocy. Zagadnie­niem tym zajmował się m.in. znako­mity niemiecki matematyk, fizyk, astronom i geodeta Carl Friedrich Gauss (1777–1855). Jego nieco zmodyfi­kowany algorytm zapisany w C++, w którym r oznacza numer roku i wszystkie zmienne są typu int, przebiega następująco:

G = r % 19;
C = r / 100;
H = (C - C/4 - (8*C + 13) / 25 + 19*G + 15) % 30;
I = H - (H/28) * (1 - (H/28) * 29 / (H+1) * (21-G) / 11);
J = (r + r/4 + I + 2 - C + C/4) % 7;
L = I - J;
M = 3 + (L+40) / 44;       // Miesiąc Wielkanocy
D = L + 28 - 31 * (M/4);   // Dzień Wielkanocy

Moduł obliczeniowy

Po każdej zmianie numeru roku, zarówno w przypadku uzyskania numeru roku bieżą­cego jak i po przejściu do roku nastę­pnego bądź poprze­dniego, należy wyznaczyć dwie daty świąt ruchomych w tablicy Rd. Po tym uaktual­nieniu nietrudno już sprawdzić, poprzez przej­rzenie jej elementów, czy data określona przez numer miesiąca i numer dnia przypada w danym roku w dzień święteczny, czy nie. Obie te operacje wygodnie jest umieścić w odrębnym module, którego plik nagłów­kowy np. o nazwie swieta.h ma postać:

// swieta.h - Moduł wyznaczania dni świątecznych
// --------------------------------------------------------------
// uzup   - uzupełnia listę świąt stałych o święta ruchome roku r
// swieto - sprawdza, czy dzień d miesiąca m jest świętem
//          (false - nie, true - tak)
// --------------------------------------------------------------

#ifndef H_SWIETA
#define H_SWIETA

void uzup(int r);
bool swieto(int m, int d);

#endif // H_SWIETA

Drugi plik modułu o nazwie swieta.cpp zawiera definicję tablicy Rd i implemen­tacje obu funkcji uzupswieto zapowie­dzianych w pliku nagłów­kowym. Kod źródłowy tej części modułu może wyglądać następująco:

#include "swieta.h"

struct dz_sw
{
    int m, d;       // miesiąc, dzień
} Rd[] =
{
    {1, 1},         // Nowy Rok
    {1, 6},         // Trzech Króli (od 2011 r.)
    {0, 0},         // Poniedziałek Wielkanocny
    {5, 1},         // Święto Pracy
    {5, 3},         // Święto Konstytucji 3 Maja
    {0, 0},         // Boże Ciało
    {8, 15},        // Wniebowzięcie NMP
    {11, 1},        // Wszystkich Świętych
    {11, 11},       // Święto Niepodległości
    {12, 25},       // Boże Narodzenie (1 dzień)
    {12, 26}        // Boże Narodzenie (2 dzień)
};

void uzup(int r)
{
    int G = r % 19,
        C = r / 100,
        H = (C - C/4 - (8*C + 13) / 25 + 19*G + 15) % 30,
        I = H - (H/28) * (1 - (H/28) * 29 / (H+1) * (21-G) / 11),
        J = (r + r/4 + I + 2 - C + C/4) % 7,
        L = I - J,
        M = 3 + (L+40) / 44,       // Miesiąc Wielkanocy
        D = L + 28 - 31 * (M/4);   // Dzień Wielkanocy

    if (D < 31)                    // Jeżeli Wielkanoc nie przypada
    {                              // 31 dnia miesiąca (31 marca),
        Rd[2].m = M;               // Poniedziałek Wielkanocny
        Rd[2].d = D + 1;           // przypada o dzień później.
    }
    else                           // W przeciwnym razie
    {                              // Poniedziałek Wielkanocny
        Rd[2].m = 4;               // przypada w następnym miesiącu,
        Rd[2].d = 1;               // tj. 1 kwietnia.
    }
    if (D > 1)                     // Jeżeli Wielkanoc nie przypada
    {                              // 1 dnia miesiąca (1 kwietnia),
        Rd[5].m = M + 2;           // Boże Ciało przypada po dwóch
        Rd[5].d = D - 1;           // miesiącach o dzień wcześniej.
    }
    else                           // W przeciwnym razie
    {                              // Boże Ciało przypada
        Rd[5].m = 5;               // w następnym miesiącu,
        Rd[5].d = 31;              // tj. 31 maja.
    }
}

bool swieto(int m, int d)
{
    for (int k = 0; k < 11; k++)
        if (m == Rd[k].m && d == Rd[k].d)
            return true;
    return false;
}

Gdy w funkcji uzup znana jest już data Wielkanocy, w pierwszej instrukcji warun­kowej if—else wyznacza się datę Ponie­działku Wielkano­cnego, w drugiej datę Bożego Ciała. Numer dnia pierwszej daty jest o 1 większy od numeru dnia daty Wielka­nocy, jeśli ten drugi jest mniejszy od 31, tj. gdy Wielkanoc nie przypada 31 marca. W prze­ciwnym razie Ponie­działek Wielka­nocny przypada 1 kwietnia. Z kolei numer dnia daty Bożego Ciała, odległej od daty Wielka­nocy o 60 dni, jest zazwyczaj o 1 mniejszy od numeru dnia daty Wielka­nocy, a numer miesiąca większy o 2. Nie jest tak tylko wtedy, gdy Wielkanoc przypada 1 kwietnia. Wówczas Boże Ciało przypada 31 maja.

Działanie funkcji swieto polega na przeszu­kiwaniu sekwenc­yjnym dat zapisanych w tablicy Rd i spraw­dzaniu, czy któraś z nich jest zgodna z datą określoną w argu­mentach funkcji. Jeśli poszuki­wana data występuje w tablicy, przeszuki­wanie kończy się powodzeniem – pętla zostaje przerwana i funkcja zwraca wartość true (dzień świąte­czny). Jeśli poszuki­wana data nie występuje w tablicy, pętla kończy się normalnie i funkcja zwraca wartość false (dzień zwykły).

Program w Borland C++

Wydaje się oczywiste, że program wyświetlający kalen­darz miesię­czny z uwyda­tnieniem świąt i dnia bieżącego innym kolorem można łatwo zbudować, modyfi­kując jego odpowie­dnik wyświe­tlania kalen­darza w jednym kolorze. Okazuje się jednak, że w Borland C++ ani wyjście strumie­niowe cout, ani funkcja biblio­teczna printf nie reagują na zmianę koloru tekstu i tła, którą umożli­wiają funkcje textcolortextback­ground biblio­teki conio. Wyjście z tej dość zadziwia­jącej sytuacji polega na użyciu funkcji cprintf z biblio­teki conio podobnej w dzia­łaniu do funkcji printf z biblio­teki stdio. Kolejną niespo­dzianką jest przecho­dzenie na początek nastę­pnego wiersza wydruku. Funkcja printf przed znakiem specjal­nym '\n' (line feed – wysuw wiersza, nowy wiersz) wypro­wadza dodatkowo znak '\r' (carriage return – powrót karetki). W przy­padku funkcji cprintf przejście na początek nastę­pnego wiersza wymaga podania pary znaków specjal­nych '\r''\n', czyli łańcucha "\r\n".

To jeszcze nie koniec niespodzianek, bowiem w prze­jętej po systemie DOS koncepcji tło okna konsoli może być tylko koloru o numerze od 0 do 7. Aby sprostać temu wyma­ganiu, wartości argu­mentów funkcji textback­ground wyższe od 7 są redu­kowane do pożąda­nego zakresu za pomocą operacji modulo 8 (% 8). Na przykład kolor WHITE o nume­rze 15 zostanie potraktowany jak LIGHTGRAY o nume­rze 7 i tło okna nie będzie wbrew oczeki­waniu białe, lecz jasno­szare.

Pełny kod źródłowy programu w języku Borland C++, który wyświetla kalen­darz miesię­czny, wyróżnia­jąc niedziele i święta kolorem jasnoczer­wonym (LIGHTRED) i datę bieżącą kolorem jasnofio­letowym (LIGHTMAGENTA), jest przedsta­wiony poniżej. W porównaniu z wersją poprzednią zmiany występują w obydwu funkcjach miesiacmain programu oraz w defi­nicji tablicy Mc nazw miesięcy.

#include <string.h>
#include <dos.h>
#include "kalend.h"
#include "klaw.h"
#include "swieta.h"

char *Mc[] = {
     "Stycze\xe4", "Luty", "Marzec", "Kwiecie\xe4", "Maj", "Czerwiec", "Lipiec",
     "Sierpie\xe4", "Wrzesie\xe4", "Pa\xab" "dziernik", "Listopad", "Grudzie\xe4"};

void miesiac(date dt, date db)
{
    clrscr();
    char *p = Mc[dt.da_mon - 1];
    gotoxy((19 - strlen(p)) / 2, 1);
    cprintf("%s %d\r\n --------------------\r\n", p, dt.da_year);
    textcolor(LIGHTRED);
    cprintf(" Nd ");
    textcolor(BLACK);
    cprintf("Po Wt \x97r Cz Pt So\r\n");
    int n = dtyg(1, dt.da_mon, dt.da_year), max = dmax(dt.da_mon, dt.da_year);
    if (n > 0)
        gotoxy(3*n + 1, wherey());
    for (dt.da_day = 1; dt.da_day <= max; dt.da_day++)
    {
        if (dt.da_day == db.da_day && dt.da_mon == db.da_mon && dt.da_year == db.da_year)
            textcolor(LIGHTMAGENTA);
        else if (swieto(dt.da_mon, dt.da_day) || dtyg(dt.da_day, dt.da_mon, dt.da_year) == 0)
            textcolor(LIGHTRED);
        cprintf("%3d", dt.da_day);
        textcolor(BLACK);
        if ((dt.da_day + n) % 7 == 0)
            cprintf("\r\n");
    }
    gotoxy(3, 11);
    cprintf("Menu: Up, Dn, Esc");
}

int main()
{
    date dt, db;        // dt - data określająca wyświetlany miesiąc
    getdate(&db);       // db - data bieżąca
    uzup(db.da_year);
    textcolor(BLACK);
    textbackground(WHITE);
    miesiac(dt = db, db);
    int c;
    while ((c = czytajZnak()) != K_ESC)
    {
        if (c == K_DN)
        {
            if (++dt.da_mon > 12)
            {
                dt.da_mon = 1;
                uzup(++dt.da_year);
            }
            miesiac(dt, db);
        }
        else if (c == K_UP)
        {
            if (--dt.da_mon < 1)
            {
                dt.da_mon = 12;
                uzup(--dt.da_year);
            }
            miesiac(dt, db);
        }
    }
}

Dodatkowa zmienna db typu struktu­ralnego date w funkcji main reprezen­tuje bieżącą datę pobie­raną z systemu na początku wykonania programu i przeka­zywaną do funkcji miesiac w drugim argu­mencie. Data przekazy­wana w jej pierw­szym argu­mencie (struktura dt) precyzuje rok (pole da_year) i miesiąc (pole da_mon), dla którego funkcja ma wyświe­tlić kalen­darz miesię­czny. Numer dnia (pole da_day) jest w tym momencie nieistotny, ale póżniej występuje w roli zmiennej sterującej pętlą for przebie­gającej wszystkie dni miesiąca. Konse­kwencją użycia funkcji cprintf zamiast standardo­wego w C++ wyjścia cout jest tworzenie odpowie­dniej wielkości odstępu przy centro­waniu nagłówka i przed nume­rem pierwszego dnia miesiąca za pomocą funkcji gotoxy, a także reprezen­towanie nazw miesięcy przez wygo­dniejsze w cprintf łańcuchy języka C niż C++.

Lista świąt jest w funkcji main wyznaczana dla bieżą­cego roku tuż po uzyskaniu aktualnej daty, a potem uaktual­niana każdora­zowo przed wyświe­tleniem następnego lub poprze­dniego miesiąca, gdy wraz z przej­ściem do niego nastą­piła zmiana numeru roku. Przed pierwszym wywołaniem funkcji miesiac ustawiane są również podstawowe kolory okna konsoli – czarny tekst (BLACK) na białym tle (WHITE). Poniższy wynik wykonania programu pokazuje, że tło okna nie jest jednak białe, lecz jasno­szare (LIGHTGRAY).

Program w MinGW C++

Rozszerzenie programu wyświetlania kalendarza miesię­cznego tak, by wyróżniał innym kolorem niedziele, święta i datę bieżącą, jest w przy­padku kompila­tora MinGW C++ nieporówny­walnie łatwiejsze niż w Borland C++. Strumień cout języka C++ i funkcja biblio­teczna printf języka C uwzglę­dniają bowiem usta­wienia kolorów tekstu i tła dokonywane przez funkcje textcolortextback­ground modułu bconio obsługi okna konsoli. Używając dwóch zmiennych dbdt typu date i sto­sując te same reguły zarzą­dzania kolorami jak w zaprezen­towanym powyżej programie w Borland C++, otrzymu­jemy następu­jącą wersję programu wyświe­tlania kalen­darza miesię­cznego w MinGW C++:

#include <iostream>
#include <iomanip>
#include <ctime>
#include "bconio.h"
#include "kalend.h"
#include "klaw.h"
#include "swieta.h"

using namespace std;

const string Mc[] = {"",
    "Stycze\xe4", "Luty", "Marzec", "Kwiecie\xe4", "Maj", "Czerwiec", "Lipiec",
    "Sierpie\xe4", "Wrzesie\xe4", "Pa\xab" "dziernik", "Listopad", "Grudzie\xe4"};

struct date
{
    int da_year;    // rok
    char da_day;    // dzień
    char da_mon;    // miesiąc
};

void getdate(date *p)
{
    time_t czas;
    time(&czas);
    tm *t = localtime(&czas);
    p->da_year = t->tm_year + 1900;
    p->da_mon = t->tm_mon + 1;
    p->da_day = t->tm_mday;
}

void miesiac(date dt, date db)
{
    clrscr();
    cout << setw((17 - Mc[int(dt.da_mon)].length()) / 2) << "" << Mc[int(dt.da_mon)]
         << setw(5) << dt.da_year << "\n --------------------\n";
    textcolor(LIGHTRED);
    cout << " Nd ";
    textcolor(BLACK);
    cout << "Po Wt \x97r Cz Pt So\n"
    int n = dtyg(1, dt.da_mon, dt.da_year), max = dmax(dt.da_mon, dt.da_year);
    if (n > 0)
        cout << setw(3 * n) << "";
    for (dt.da_day = 1; dt.da_day <= max; dt.da_day++)
    {
        if (dt.da_day == db.da_day && dt.da_mon == db.da_mon && dt.da_year == db.da_year)
            textcolor(LIGHTMAGENTA);
        else if (swieto(dt.da_mon, dt.da_day) || dtyg(dt.da_day, dt.da_mon, dt.da_year) == 0)
            textcolor(LIGHTRED);
        cout << setw(3) << int(dt.da_day);
            textcolor(BLACK);
        if ((dt.da_day + n) % 7 == 0)
            cout << endl;
    }
    gotoxy(3, 11);
    cout << "Menu: Up, Dn, Esc";
}

int main()
{
    date dt, db;        // dt - data określająca wyświetlany miesiąc
    getdate(&db);       // db - data bieżąca
    uzup(db.da_year);
    textcolor(BLACK);
    textbackground(WHITE);
    miesiac(dt = db, db);
    int c;
    while ((c = czytajZnak()) != K_ESC)
    {
        if (c == K_DN)
        {
            if (++dt.da_mon > 12)
            {
                dt.da_mon = 1;
                uzup(++dt.da_year);
            }
            miesiac(dt, db);
        }
        else if (c == K_UP)
        {
            if (--dt.da_mon < 1)
            {
                dt.da_mon = 12;
                uzup(--dt.da_year);
            }
            miesiac(dt, db);
        }
    }
}

Zauważmy, że wyprowadzenie w funkcji miesiac wartości pola dt.da_day (numeru dnia) do strumienia cout wymaga rzuto­wania (konwersji) wartości typu char (znak) na wartość typu int (kod znaku). Gdyby rzuto­wania nie było, na wyjściu pojawiłby się dziwny znak zamiast numeru dnia. Rzuto­wanie wystę­puje jeszcze dwukro­tnie w tej samej funkcji – w operacji wydruku nagłówka z nazwą miesiąca. Tym razem ma ono na celu jedynie uniknięcie ostrze­żenia kompila­tora informu­jącego, że indeksem elementu tablicy jest wartość pola dt.da_mon (numer miesiąca) typu char. Załączony poniżej przykła­dowy wynik wykonania programu pokazuje, że redukcja kolorów tła okna właściwa kompila­torowi Borland C++ w przy­padku kompila­tora MinGW C++ nie występuje.

Program w Visual C++

Rozszerzenie programu wyświetlania kalendarza miesięcznego w Visual C++ o wyróż­nianie innym kolorem niedziel, świąt i daty bieżącej nie sprawia żadnych kłopotów, gdyż przebiega dokładnie tak samo jak jego odpo­wiednika w MinGW C++. Ostate­czna wersja komple­tnego programu wygląda następująco:

#include <iostream>
#include <iomanip>
#include <ctime>
#include <string>
#include "bconio.h"
#include "kalend.h"
#include "klaw.h"
#include "swieta.h"

using namespace std;

const string Mc[] = {"",
    "Stycze\xe4", "Luty", "Marzec", "Kwiecie\xe4", "Maj", "Czerwiec", "Lipiec",
    "Sierpie\xe4", "Wrzesie\xe4", "Pa\xab" "dziernik", "Listopad", "Grudzie\xe4"};

struct date
{
    int da_year;    // rok
    char da_day;    // dzień
    char da_mon;    // miesiąc
};

void getdate(date *p)
{
    time_t czas;
    time(&czas);
    tm t;
    localtime_s(&t, &czas);
    p->da_year = t.tm_year + 1900;
    p->da_mon = t.tm_mon + 1;
    p->da_day = t.tm_mday;
}

void miesiac(date dt, date db)
{
    clrscr();
    cout << setw((17 - Mc[dt.da_mon].length()) / 2) << "" << Mc[dt.da_mon]
         << setw(5) << dt.da_year << "\n --------------------\n";
    textcolor(LIGHTRED);
    cout << " Nd ";
    textcolor(BLACK);
    cout << "Po Wt \x97r Cz Pt So\n"
    int n = dtyg(1, dt.da_mon, dt.da_year), max = dmax(dt.da_mon, dt.da_year);
    if (n > 0)
        cout << setw(3 * n) << "";
    for (dt.da_day = 1; dt.da_day <= max; dt.da_day++)
    {
        if (dt.da_day == db.da_day && dt.da_mon == db.da_mon && dt.da_year == db.da_year)
            textcolor(LIGHTMAGENTA);
        else if (swieto(dt.da_mon, dt.da_day) || dtyg(dt.da_day, dt.da_mon, dt.da_year) == 0)
            textcolor(LIGHTRED);
        cout << setw(3) << int(dt.da_day);
            textcolor(BLACK);
        if ((dt.da_day + n) % 7 == 0)
            cout << endl;
    }
    gotoxy(3, 11);
    cout << "Menu: Up, Dn, Esc";
}

int main()
{
    date dt, db;        // dt - data określająca wyświetlany miesiąc
    getdate(&db);       // db - data bieżąca
    uzup(db.da_year);
    textcolor(BLACK);
    textbackground(WHITE);
    miesiac(dt = db, db);
    int c;
    while ((c = czytajZnak()) != K_ESC)
    {
        if (c == K_DN)
        {
            if (++dt.da_mon > 12)
            {
                dt.da_mon = 1;
                uzup(++dt.da_year);
            }
            miesiac(dt, db);
        }
        else if (c == K_UP)
        {
            if (--dt.da_mon < 1)
            {
                dt.da_mon = 12;
                uzup(--dt.da_year);
            }
            miesiac(dt, db);
        }
    }
}

Jak widać, jedyna różnica, oprócz przejętej po poprzedniej wersji programu wyświe­tlania kalen­darza funkcji getdate pobiera­jącej datę bieżącą z systemu, tkwi w pominięciu zbędnego w przy­padku kompila­tora Visual C++ rzuto­wania występu­jącej w roli indeksu elementów tablicy Mc wartości pola dt.da_mon typu char na wartość typu int. Kompilator Visual C++ okazał się tym razem mniej rygory­styczny od MinGW C++.


Opracowanie przykładu: październik 2018