1. Formatowanie, lokalizacja i internacjonalizacja

Programy powinny prezentować wartości liczb, dat, czasu w określonych formatach, Formaty te zależą od  kraju lub regionu (języka używanego w kraju/regionie oraz innych kulturowo utrwalonych reguł). Krótko mówiąc zależą od lokalizacji . Również treść komunikatów wyświetlanych przez program lub opisów jakichś elementów GUI (a nawet ikon czy dźwięków używanych w programie) jest zależna od lokalizacji. Zatem - z jednej strony - programy powinny być dostosowane do konkretnych lokalizacji.  Z drugiej strony często powstaje potrzeba przygotowania programów w taki sposób, by - bez rekompilacji - mogły działać w różnych lokalizacjach. Tym właśnie zagadnieniom - szczególnie istotnym w takich krajach jak Polska - poświęcony jest niniejszy rozdział. Równoczesnie  warto zaznaczyć, że także poza sferą internacjonalizacji i bez specjalnego zajmowania się ustawieniami regionalnymi prezentowane tu narzędzia stanowią istotny element warstzatu programistycznego. Bez nich niemożliwe jest bowiem odpowiednie formatowanie liczb i operowanie na datach.


1.1. Lokalizacje

W języku angielskim okreslane jest to terminem locale. Po polsku nie ma dobrego odpowiednika, używamy więc słowa lokalizacja zarówno w znaczeniu locale jak i  dla określenia sposobu oprogramowania aplikacji w taki sposób, by działała ona własciwie dla danych ustawień regionalnych
Specyficzne dla danego języka, regionu/kraju - reguły, dotyczące prezentacji różnych informacji (np.formatowania liczb i dat, pisowni tekstów, porządku alfabetycznego) nazwiemy lokalizacją.

Do rozwiązywania zagadnień lokalizacyjnych służy w Javie cały zestaw klas (skupiony przede wszystkim w pakiecie java.text, ale nie tylko). Zestaw ten został opracowany przez firmę Taligent, wchodzącą w skład koncernu IBM i wdrożony przez Sun w Javie w wersji 1.1,  Bez większych zmian przetrwał on w kolejnych wersjach Javy do dziś, natomiast zespół  lokalizacyjny z Taligent przekształcił się w Unicode group w IBM Globalization Center of Competency w Cupertino i zajął się doskonaleniam i rozwijaniem swojego pierwotnego produktu oraz przenoszeniem go do języków C++ i C, a wszystko na zasadach "open source".
W efekcie powstała biblioteka o nazwie International Components for Unicode, obejmujący poszerzone wersje klas lokalizacyjno-internacjonalizacyjnych,  a także pewne dodatkowe klasy, których w standardzie Javy brak. Biblioteka wdraża najnowsze standardy Unicode (w tej chwili Unicode 4.0) i  jest dostępna w wersjach dla języków Java (ICU4J),  C++ i C (ICU4C) na stronie http://oss.software.ibm.com/icu/.

Generalnie będziemy się tu zajmować standardowymi klasami Javy, gdzieniegdzie tylko przywołując dodatki i rozszerzenia z ICU4J

W Javie lokalizacje reprezentowane są przez obiekty klasy Locale z pakietu java.util.

Lokalizacja określana jest przez kombinację:
Kody te - wartości typu String -  podajemy przy tworzeniu obiektu klasy Locale jako argumenty konstruktora tej klasy, przy czym mamy do dyspozycji trzy przeciążone konstruktory:
Locale(String language)          
Locale(String language, String country)
Locale(String language, String country, String variant)

Kod języka - to kombinacja dwóch małych liter, określająca język wg standardu ISO-639 (zob dostępne kody : http://www.ics.uci.edu/pub/ietf/http/related/iso639.txt)
Kod kraju - to dwuliterowa kombinacja dwóch dużych liter, określająca kraj wg standardu ISO-3166 (http://www.chemie.fu-berlin.de/diverse/doc/ISO_3166.html)
Kod wariantu - jest dodatkową  informacją, którą możemy dodać i która nie musi spełniać żadnych standardów, wobec czego jest specyficzna w danych warunkach (np. pakietu lokalizacyjnego) lub dla danej aplikacji.


Przykłady:

Locale a = new Locale("en", "GB");  // język angielski, kraj Wielka Brytania
Locale b = new Locale("en", "US");  // język angielski, kraj Stany Zjednoczone
Locale c = new Locale("en");            // język angielski, kraj nieokreślony

Locale d = new Locale("pl", "PL", "Zakopane");
// Powyżej definiujemy wariant lokalizacyjny - "Zakopane"
// język jest polski, kraj - Polska
// ale dodatkowo nasza aplikacja może skorzystać ze zdefiniowanego wariantu
// jednak tylko nasze programy będą wiedziały co z takim wariantem należy robić
// może np. wyświetlać komunikaty w gwarze?
Uwaga: dla niektórych lokalizacji w Javie są określone warianty. Np. tradycyjny Tajski, albo -  w pakietach ICU4J - dla krajów Europejskiej Unii Monetarnej  warianty PREEURO (przed wprowadzeniem euro).

Obiekt klasy Locale określa lokalizację (czyli wspomniane wcześniej reguły),
Zastosowanie tych reguł - przy przetwarzaniu  i formatowaniu informacji - spoczywa na obiektach innych klas. Te klasy, które biorą pod uwagę wymagania lokalizacyjne nazywają się czułymi na lokalizację (locale-sensitive).

Należą do nich:

Klasa
Przeznaczenie
NumberFormat (i pochodne)
Do formatowania liczb
Calendar (i pochodne)
Do operowania na datach i czasie
DateFormat (i pochodne)
Do formatowania dat i czasu
Collator
Do określania porządku alfabetycznego
BreakIterator
Do zlokalizowanego rozbioru tekstu

Każdy program w Javie - przy uruchomieniu, na podstawie właściwości ustalonych dla platformy systemowej  - uzyskuje tzw. domyślną lokalizację .  Możemy się dowiedzieć jaka to jest lokalizacja za pomocą odwołania Locale.getDefault() i możemy zmienić tę lokalizację używając metody Locale.setDefault(Locale).

Aby przetwarzać informacje w  zlokalizowanej formie posługujemy się obiektami klas czułych na lokalizację. Dla zastosowania domyślnych reguł lokalizacyjnych uzyskujemy te obiekty za pomocą statycznych metod get...Instance() bez argumentu, określającego lokalizację.
Np. kod na listingu pokazuje domyślną lokalizację i zgodnie z tą lokalizacją wypisuje bieżącą datę oraz liczbę 1234567.1, przy czym w trakcie działania zmienia domyślną lokalizację i ponawia wyprowadzanie informacji.
import java.text.*;
import java.util.*;

public class DefLok {

  static public void report() {
    Locale defLoc = Locale.getDefault();
    System.out.println("Domyślna lokalizacja : " + defLoc);
    DateFormat df = DateFormat.getDateInstance(DateFormat.LONG);
    NumberFormat nf = NumberFormat.getInstance();
    System.out.println(df.format(new Date()));
    System.out.println(nf.format(1234567.1));
  }

  public static void main(String[] args) {
    report();
    Locale.setDefault(new Locale("en"));
    report();
  }

}
 Wydruk programu:

Domyślna lokalizacja : pl_PL
12 lipiec 2003
1_234_567,1
Domyślna lokalizacja : en
July 12, 2003
1,234,567.1


Zwróćmy uwagę:
Klasy lokalizacyjnie-czułe pozwalają również na uzyskiwanie ich obiektów przetwarzających informacje w sposób wymagany przez konkretną (nie domyślną) lokalizację, W tym celu używamy statycznych metod get...Instance(...) z argumentem typu Locale - określającym konkretną lokalizację.

Np. poniższy program wyprowadz datę w lokalizacji domyślnej, a liczbę - najpierw w domyślnej, a później zgodnej z językiem angielskim.
import java.text.*;
import java.util.*;

public class MiscLok {

  public static void main(String[] args) {
    System.out.println("Domyślna lokalizacja : " + Locale.getDefault());
    DateFormat df = DateFormat.getDateInstance(DateFormat.LONG);
    System.out.println(df.format(new Date()));
    double num = 123.4;
    NumberFormat nf = NumberFormat.getInstance();
    System.out.println("Liczba " + num +
                       " w lokalizacji domyślnej: " + nf.format(num));
    nf = NumberFormat.getInstance(new Locale("en"));
    System.out.println("Liczba " + num +
                       " w lokalizacji angielskiej: " + nf.format(num));
  }

}

Domyślna lokalizacja : pl_PL
12 lipiec 2003
Liczba 123.4 w lokalizacji domyślnej: 123,4
Liczba 123.4 w lokalizacji angielskiej: 123.4

Uwaga: liczby różnią się separatorem miejsc dziesiętnych (przecinek dla Polski, kropka dla krajów anglojęzycznych).

Zatem możemy - w trakcie działania programu - ustalać lokalizacje zarówno ogólnie (dla wszystkich klas lokalizacyjnie-czułych), jak i dla każdego konkretnego obiektu tych klas.

Jednak nie wszystkie możliwe lokalizacje (określone w standardzie ISO) są przez Javę podtrzymywane (nie dla wszystkich zdefiniowano reguły formatowania liczb i czasu czy też  traktowania tekstów).

Dostępne z poziomu Javy lokalizacje można uzyskać za pomocą metod Locale[] getAvailableLocales() , zdefiniowanych w klasach Locale oraz wszystkich klasach czułych na lokalizację. Metody te zwracają tablice dostępnych lokalizacji. Generalnie, będą to te same zestawy lokalizacji.

Poniższy program wypisuje wszystkie dostępne lokalizacje.

import java.util.*;
import java.text.*;

public class Lokal1 {

  public static void main(String[] args) {

    // Tablica dostępnych lokalizacji

    Locale[] loc = Locale.getAvailableLocales();

    System.out.println("Kod języka" + "#" +
                       "Kod kraju" + "#" +
                       "Kod wariantu" +  "#" +
                       "Język" + "#" +
                       "Kraj" +  "#" + "Wariant"
                      );

    for (int i=0; i<loc.length; i++) {
      String countryCode = loc[i].getCountry();  // kod kraju
      String langCode = loc[i].getLanguage();    // kod języka
      String varCode  = loc[i].getVariant();     // wariant

      // lokalizacja opisana w języku domyślnej lokalizacji (polskim)
      String kraj =  loc[i].getDisplayCountry();
      String jezyk = loc[i].getDisplayLanguage();
      String wariant = loc[i].getDisplayVariant();
      System.out.println(langCode + "#" +
                         countryCode +  "#" +
                         varCode + "#" +
                         jezyk + "#" + kraj + "#" + wariant);

     }
  }

}
Uwaga: warto w tym programie zwrócić uwagę na użycie różnych metod wobec obiektów klasy Locale.
Wynik działania programy pokazuje tablica (będąca jednocześnie przewodnikeim po aktualnie dostępnych w Javie lokalizacjach).

Kod językaKod krajuKod wariantuJęzykKrajWariant
ar

arabski

arAE
arabskiZjednoczone Emiraty Arabskie
arBH
arabskiBahrajn
arDZ
arabskiAlgeria
arEG
arabskiEgipt
arIQ
arabskiIrak
arJO
arabskiJordan
arKW
arabskiKuwejt
arLB
arabskiLiban
arLY
arabskiLibia
arMA
arabskiMaroko
arOM
arabskiOman
arQA
arabskiKatar
arSA
arabskiArabia Saudyjska
arSD
arabskiSudan
arSY
arabskiSyria
arTN
arabskiTunezja
arYE
arabskiJemen
be

białoruski

beBY
białoruskiBiałoruś
bg

bułgarski

bgBG
bułgarskiBułgaria
ca

kataloński

caES
katalońskiHiszpania
cs

czeski

csCZ
czeskiRepublika Czeska
da

duński

daDK
duńskiDania
de

niemiecki

deAT
niemieckiAustria
deCH
niemieckiSzwajcaria
deDE
niemieckiNiemcy
deLU
niemieckiLuksemburg
el

grecki

elGR
greckiGrecja
enAU
angielskiAustralia
enCA
angielskiKanada
enGB
angielskiWielka Brytania
enIE
angielskiIrlandia
enIN
angielskiIndie
enNZ
angielskiNowa Zelandia
enZA
angielskiRepublika Południowej Afryki
es

hiszpański

esAR
hiszpańskiArgentyna
esBO
hiszpańskiBoliwia
esCL
hiszpańskiChile
esCO
hiszpańskiKolumbia
esCR
hiszpańskiKostaryka
esDO
hiszpańskiRepublika Dominikany
esEC
hiszpańskiEkwador
esES
hiszpańskiHiszpania
esGT
hiszpańskiGwatemala
esHN
hiszpańskiHonduras
esMX
hiszpańskiMeksyk
esNI
hiszpańskiNikaragua
esPA
hiszpańskiPanama
esPE
hiszpańskiPeru
esPR
hiszpańskiPortoryko
esPY
hiszpańskiParagwaj
esSV
hiszpańskiSalwador
esUY
hiszpańskiUrugwaj
esVE
hiszpańskiWenezuela
et

estoński

etEE
estońskiEstonia
fi

fiński

fiFI
fińskiFinlandia
fr

francuski

frBE
francuskiBelgia
frCA
francuskiKanada
frCH
francuskiSzwajcaria
frFR
francuskiFrancja
frLU
francuskiLuksemburg
hiIN
hindiIndie
hr

chorwacki

hrHR
chorwackiChorwacja
hu

węgierski

huHU
węgierskiWęgry
is

islandzki

isIS
islandzkiIslandia
it

włoski

itCH
włoskiSzwajcaria
itIT
włoskiWłochy
iw

hebrajski

iwIL
hebrajskiIzrael
ja

japoński

jaJP
japońskiJaponia
ko

koreański

koKR
koreańskiKorea Południowa
lt

litewski

ltLT
litewskiLitwa
lv

łotewski

lvLV
łotewskiŁotwa
mk

macedoński

mkMK
macedońskiMacedonia
nl

holenderski

nlBE
holenderskiBelgia
nlNL
holenderskiHolandia
no

norweski

noNO
norweskiNorwegia
noNONYnorweskiNorwegiaNynorsk
pl

polski

plPL
polskiPolska
pt

portugalski

ptBR
portugalskiBrazylia
ptPT
portugalskiPortugalia
ro

rumuński

roRO
rumuńskiRumunia
ru

rosyjski

ruRU
rosyjskiRosja
sh

serbo-chorwacki

shYU
serbo-chorwackiJugosławia
sk

słowacki

skSK
słowackiSłowacja
sl

słoweński

slSI
słoweńskiSłowenia
sq

albański

sqAL
albańskiAlbania
sr

serbski

srYU
serbskiJugosławia
sv

szwedzki

svSE
szwedzkiSzwecja
th

tajlandzki

thTH
tajlandzkiTajlandia
thTHTHtajlandzkiTajlandiaTH
tr

turecki

trTR
tureckiTurcja
uk

ukraiński

ukUA
ukraińskiUkraina
zh

chiński

zhCN
chińskiChiny
zhHK
chińskiHong Kong
zhTW
chińskiTajwan
en

angielski

enUS
angielskiStany Zjednoczone Ameryki
Uwaga:  tabela pokazuje lokalizacje dostępne w Javie 1.4.1. Przy użyciu biblioteki ICU4J uzyskamy większy zestaw dostępnych lokalizacji.

Należy tu zaobserwować następujące cechy:

Poniższy przykładowy program pokazuje wykorzystanie prostej klasy Locale w celu tłumaczenia nazw krajów na różne języki.
import java.util.*;
import java.text.*;
import javax.swing.*;

class CountryTranslator {

  public static void main(String[] args) {

    Locale[] loc = Locale.getAvailableLocales();
    Map map = new HashMap();
    String kraj;

    // Dodanie dostępnych lokalizacji do mapy
    // klucz: nazwa kraju po polsku, wartośc - lokealizacja
    for (int i=0; i<loc.length; i++) {
      String countryCode = loc[i].getCountry();  // kod kraju
      if (countryCode.equals("")) continue;
      kraj =  loc[i].getDisplayCountry();
      map.put(kraj, loc[i]);
    }

    String msg = "Podaj kraj";
    String in = "";
    while((kraj = JOptionPane.showInputDialog(msg)) != null ) {
      // Pobieramy lokalizację dla podanego kraju
      Locale savedLoc = (Locale) map.get(kraj);
      if (savedLoc == null) continue;
      msg = "Podaj kody języków, rozdzielone spacjami";
      while((in = JOptionPane.showInputDialog(null, msg, in)) != null ) {
        StringTokenizer st = new StringTokenizer(in);
        if (st.countTokens() == 0) continue;
        String rep = "Nazwa kraju " + kraj + ":\n";

        // Dla kolejnych kodów języków
        // uzyskujemy nazwę kraju w języku odpowiadającym
        // lokalizacji związanej z kodem jęsyka
        while(st.hasMoreTokens()) {
          Locale lang = new Locale(st.nextToken());
          rep += lang.getDisplayLanguage() + "   =   " +
                 savedLoc.getDisplayCountry(lang) + "\n";
        }
      JOptionPane.showMessageDialog(null,rep);
      }
      msg = "Podaj kraj";
    }
    System.exit(0);
  }
}
r W tym programie wykorzystaliśmy fakt, iż metody getDisplay... użyte bez argumentów zwracają opisy w języku lokalizacji domyślnej (np. loc.getDisplayLanguage() zwróci opis języka po polsku), a użyte z argumentem-lokalizacją zwracają opis w języku podanej lokalizacji (np. loc1.getDisplayCountry(loc2) zwróci nazwę kraju, związanego z lokalizacją loc1 w języku lokalizacji loc2).
Przykładowy wynik działania programu pokazano na rysunku.

1.2. Formatowanie liczb

Gdy wyprowadzamy liczby rzeczywiste w postaci znakowej np. za pomocą metody System.out.println(...), to nie mamy wpływu na ich formatowanie (np. ile jest pokazywanych miejsc dziesiętnych). Np. wynikiem takiego fragmentu programu:

     double d = 10/3.0;
     System.out.println(d);

będzie:

3.3333333333333335

Bardzo często będziemy potrzebować "sformatowanego" wydruku liczby (np. z trzema miejscami dziesiętnymi)
W pakiecie java.text znajdziemy klasy, które umożliwiają formatowanie liczb.

Aby w powyższym przykładzie uzyskać trzy miejsca dziesiętne możemy np. użyć klasy NumberFormat:
     NumberFormat nf = NumberFormat.getInstance();
     nf.setMaximumFractionDigits(3);
     String wynik = nf.format(d);
     System.out.println(wynik);

Tutaj:
Wygodniejszy i bardziej uniwersalny sposób formatowania liczb polega na specyfikowaniu wzorców formatu. Mogą one być stosowane wobec formatorów, które są obiektami klasy DecimalFormat.

Klasa DecimalFormat jest podklasą klasy NumberFormat, a metoda getInstance() z klasy NumberFormat tak naprawdę zwraca referencję do obiektu tej właśnie podklasy.

Możemy postąpić tak:
Na przykład:
     double d = 10/3.0;
     DecimalFormat dform = new DecimalFormat("###.###");
     String wynik = dform.format(d);
     System.out.println(wynik);
Tutaj otrzymamy wynik: 3,333, bowiem zastosowany wzorzec formatowania mówi o tym, że napis reprezentujący liczbę ma zawierać cyfry na pozycjach calkowitych (przy czym wiodące zera nie będą pokazywane, a liczba cyfr będzie odpowiednia dla wyniku), znak reprezentujący separator miejsc dziesiętnych oraz co najwyżej trzy cyfry po przecinku (przy czym nieznaczące zera nie będą pokazywane, a ostatnia pokazana cyfra będzie zaokrąglona).

Wzorce formatowania są łańcuchami znakowymi i mają następującą postać:

[prefiks][częśc_całkowita][.część_dziesiętna][sufiks]

gdzie:
Uwagi:
  1. nawiasy kwadratowe oznaczają opcjonalność elememetu wzorca, z tym, że co najmniej jeden z elementów musi wystąpić;
  2. jako wzorzec formatujący można podac dwa wzorce w powyższej postaci, rozdzielone średnikiem; pierwszy z nich będzie dotyczył liczb dodatnich, drugi - ujemnych.


Najważniejsze znaki specjalne używane we wzorcach formatujących podaje tabela.

SymbolOpis
0cyfra, jeśli jest nieznaczącym zerem pokazywana jako 0
#cyfra, nieznaczące zera nie są pokazywane
.miejsce separatora dziesiętnego
,miejsce separatora grup cyfr (np. tysięcy)
Emiejsce separatora dla notacji  inżynieryjnej lub naukowej ( np. 1E-11)
;separator formatu dla liczb dodatnich i formatu dla liczb ujemnych
< /td>
-znak minus
%powoduje mnożenie liczby przez 100 i pokazanie jej w postaci procentowej
¤symbol waluty (np. zł); użyty dwukrotnie daje międzynarodowy symbol waluty
'ujęte w apostrofy znaki specjalne mogą być pokazywane w części prefiks lub sufiks

Program na wydruku pokazuje jak można korzystać z różnych formatów.
import java.text.*;
import java.math.*;

public class Format1 {

  public static void show(double n1, Double n2, BigDecimal n3,
                          String format) {
    DecimalFormat df = new DecimalFormat(format);
    System.out.println("Format " + format);
    System.out.println("Liczba: " +  n1 + " wygląda tak: " + df.format(n1));
    System.out.println("Liczba: " +  n2 + " wygląda tak: " + df.format(n2));
    System.out.println("Liczba: " +  n3 + " wygląda tak: " + df.format(n3));
  }


  public static void main(String[] args) {
    double num1  = 1.346;
    Double num2  = new Double(0.765474);
    BigDecimal num3 = new BigDecimal("100.2189091");

    show(num1, num2, num3, "#.##");
    show(num1, num2, num3, "#.## %");
    show(num1, num2, num3, "#.0000");
    show(num1, num2, num3, "#.00 ¤");
    show(num1, num2, num3, "#.00 ¤¤");
    show(num1, num2, num3, "[ 000.0 ]");

 }

}
Wyniki jego dzialania pokazuje wydruk.

Format #.##
Liczba: 1.346 wygląda tak: 1,35
Liczba: 0.765474 wygląda tak: 0,77
Liczba: 100.2189091 wygląda tak: 100,22
Format #.## %
Liczba: 1.346 wygląda tak: 134,6 %
Liczba: 0.765474 wygląda tak: 76,55 %
Liczba: 100.2189091 wygląda tak: 10021,89 %
Format #.0000
Liczba: 1.346 wygląda tak: 1,3460
Liczba: 0.765474 wygląda tak: ,7655
Liczba: 100.2189091 wygląda tak: 100,2189
Format #.00 ¤
Liczba: 1.346 wygląda tak: 1,35 zł
Liczba: 0.765474 wygląda tak: ,77 zł
Liczba: 100.2189091 wygląda tak: 100,22 zł
Format #.00 ¤¤
Liczba: 1.346 wygląda tak: 1,35 PLN
Liczba: 0.765474 wygląda tak: ,77 PLN
Liczba: 100.2189091 wygląda tak: 100,22 PLN
Format [ 000.0 ]
Liczba: 1.346 wygląda tak: [ 001,3 ]
Liczba: 0.765474 wygląda tak: [ 000,8 ]
Liczba: 100.2189091 wygląda tak: [ 100,2 ]


Zwrócmy uwagę, że za pomocą metody format(...) można formatowac nie tylko liczby typu double, ale również typu long oraz obiekty klas pochodnych od Number (np. Double, Float, Long, Integer) i BigInteger oraz BigDecimal.

Klasy formatujące liczby są przygotowane na prezentację liczb według reguł lokalizacyjnych. Jeśli w metodzie getInstance() nie podamy lokalizacji - będzie użyta lokalizacja domyślna, w naszym przypadku polska, wedle której separatorem miejsc dziesiętnych jest przecinek.
Jak uzyskać kropkę zamiast przecinka? Możemy zmienić lokalizację na taką, w której seperatorem jest kropka (np. angielską) np. tak:
    DecimalFormat df = (DecimalFormat)
                       NumberFormat.getInstance(new Locale("en", "US"));
    df.applyPattern(format);

Ale wtedy utracimy polskie symbole waluty,  zamiast nich pojawią się dla Stanów Zjednoczonych USD, dla innych anglojęzycznych krajów inne symbole, a jeśli nie podamy kraju - XXX (nieznana waluta).

Możemy co prawda ustalić walutę za pomocą metody setCurrency(...), użytej wobec formatora, ale wykorzystajmy raczej to, że
Zmiana separatora miejsc dziesiętnych na kropkę może wyglądać  tak:
DecimalFormat df = new DecimalFormat(format);  // formator w domyślnej lokalizacji
DecimalFormatSymbols sym = df.getDecimalFormatSymbols(); // symbole
sym.setDecimalSeparator('.');                  // ustalenie separatora miejsc dziesiętnych
Teraz nasz program wyprowadzi np. zamiast 1,35 zł  napis 1.35 zł.
Inne symbole używane przy formatowaniu i metody ich zmian opisane są w dokumentacji.

Zwykle jednak będziemy chcieli wypisywać liczby w takiej postaci, w jakiej przyjęto dla danej lokalizacji i tutaj rola formatorów jest nieoceniona, łatwo bowiem ustalić potrzebną lokalizację (oprócz wspomnianych wcześniej sposobów można też stworzyć obiekt klasy DecimalFormatSymbols dla lokalizacji określanej przez argument konstruktora tej klasy i użyć tego obiektu przy tworzeniu formatora DecimalFormat - konstruktor DecimalFormat(String format, DecimalFormatSymbols symbole)).

Zwróćmy uwagę, że metoda getInstance(...) klasy NumberFormat jest metodą fabryczną - zwraca obiekt określonej podklasy klasy NumberFormat. Obecnie jest to obiekt klasy DecimalFormat, ale - jak napisano w dokumentacji -  to niekoniecznie musi być zagwarantowane w przyszłości, tak że dla większej przenośności kody należało by przed formatowaniem dziesiętnym sprawdzać czy zwrócony przez getInstance() obiekt jest obiektem klasy DecimalFormat.
Możemy też uzyskać specjalne formatory (będące teraz również obiektami klasy DecimalFormat, ale o specjalnych właściwościach) do formatowania:
Te swoiste warianty są jak gdyby prostszą drogą uzyskiwania efektów podobnych do użycia wzorców formatowania, o ile zależy nam tylko na tych właśnie (całkowitych, walutowych, procentowych) formatach.
Oczywiście, wszystkie te metody mają wersje z argumentem okreslającym lokalizację.

Formatory potrafią także dokonywać przekształceń odwrotnych: zamieniać napisy reprezentujące liczby na postać binarną tych liczb. Istotnie, jeśli mam jakiś tekst, w którym liczby podawane są z przecinkami jako separatorami miejsc dziesiętnych, to metoda parseDouble z klasy Double nie da oczekiwanych wyników. Zamiast niej możemy zastosować metodę parse(..) zdefiniowaną w klasach formatorów i metoda ta poradzi sobie z dowolnymi sposobami zapisu liczb wedle różnych reguł lokalizacyjnych (a także wedle różnych formatów).

Do zamiany napisów reprezentujących liczby w danym formacie na ich binarną postać można zastosować metodę parse(String) użytą na rzecz formatora.
Przy tym:
Program na listingu pokazuje prosty przykład użycia metody parse(...).
import javax.swing.*;
import java.text.*;

public class Parse1 {

  public static void main(String[] args) {
    NumberFormat format = NumberFormat.getInstance();
    String in,
           msg = "Podaj liczbę";
    Number num = null;
    while ((in = JOptionPane.showInputDialog(msg)) != null) {
      System.out.println("Wejście: " + in);
      try {
        num = format.parse(in);
      } catch (ParseException exc) {
          System.out.println("Wadliwe dane: " + in);
          System.out.println(exc);
          continue;
      }
      System.out.println("Parse daje: " +
                          num.getClass().getName()+ " = " + num);
    }
    System.exit(0);
  }

}
Na wydruku przedstawiono efekty działania programu dla różnych danych wejściowych:

Wejście: 1
Parse daje: java.lang.Long = 1
Wejście: 1.111
Parse daje: java.lang.Long = 1
Wejście: 1,111
Parse daje: java.lang.Double = 1.111
Wejście: 1e16
Parse daje: java.lang.Long = 1
Wejście: 99999999999999999999999999999999999
Parse daje: java.lang.Double = 1.0E35
Wejście: 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999888888888888888888888888888888888888888888888888888888899999999999999999999999999999999999999999999999999
Parse daje: java.lang.Double = Infinity
Wejście: -1,213
Parse daje: java.lang.Double = -1.213
Wejście: 212a
Parse daje: java.lang.Long = 212
Wejście: aaaa
Wadliwe dane: aaaa
java.text.ParseException: Unparseable number: "aaaa"


Gdybyśmy w tym programie zamienili format na:

NumberFormat format = NumberFormat.getCurrencyInstance();
to:
wprowadzenie napisu 23 - dałoby wyjątek ParseException
wprowadzenie napisu 23 PLN - dałoby wyjątek ParseException
wprowadzenie napisu 23zł - dałoby wyjątek ParseException
i tylko po wprowadzeniu właściwiego domyślnego formatu walutowego:
23 zł
uzyskalibyśmy liczbę 23.

Jeśli natomiast użylibyśmy wzorca formatowania:

NumberFormat format = new DecimalFormat("[ #.0000 ]");

to akceptowane byłyby wyłącznie napisy, reprezentujące poprawne liczby ujęte w nawiasy kwadratowe (z okalającymi spacjami).

W tych przypadkach, gdy błąd może pojawić się nie tylko na samym początku napisu, użyteczna może okazać się metoda getErrorOffset() z klasy ParseException, która zwraca pozycję w napisie, na której pojawił się błąd.

Zobaczmy na przykładzie zmodyfikowanego programu:
  public static void main(String[] args) {
    NumberFormat format = new DecimalFormat("[ #.0000 ]");
    //...
    while ((in = JOptionPane.showInputDialog(msg)) != null) {
      System.out.println("Wejscie: " + in);
      try {
        num = format.parse(in);
      } catch (ParseException exc) {
          System.out.println("Wadliwe dane: " + in);
          System.out.println(exc);
          System.out.println("Wadliwa pozycja: " + exc.getErrorOffset());
          continue;
      }
      System.out.println("Parse daje: " +
                          num.getClass().getName()+ " = " + num);
    }
  }

Przykładowy wydruk zmodyfikowanego programu :

Wejscie: [23]
Wadliwe dane [23]
java.text.ParseException: Unparseable number: "[23]"
Wadliwa pozycja: 0
Wejscie: [ 23 ]
Parse daje: java.lang.Long = 23
Wejscie: [ 23
Wadliwe dane: [ 23
java.text.ParseException: Unparseable number: "[ 23 "
Wadliwa pozycja: 4
Wejscie: [ 23 a ]
Wadliwe dane: [ 23 a ]
java.text.ParseException: Unparseable number: "[ 23 a ]"
Wadliwa pozycja: 4
Wejscie: [ 23.000 ]
Wadliwe dane: [ 23.000 ]
java.text.ParseException: Unparseable number: "[ 23.000 ]"
Wadliwa pozycja: 4
Wejscie: [ 23, 00 ]
Wadliwe dane: [ 23, 00 ]
java.text.ParseException: Unparseable number: "[ 23, 00 ]"
Wadliwa pozycja: 5
Wejscie: [ 23,0 ]
Parse daje: java.lang.Long = 23
 


Inną formą metody parse z klas formatorów jest:

    Number num = parse(String dane, ParsePosition pos);

Tutaj używamy obiektu pos klasy ParsePosition, który określa bieżącą pozycję rozbioru napisu dane oraz ew. pozycję (indeks) na której wystąpił błąd.
Rozbiór danych (wedle formatu) rozpoczyna się od pozycji okreslonej przez podany obiekt klasy ParsePosition. Napis podlega interpretacji (dopóki kolejne jego znaki można traktować jako znaki liczby wg danego formatu), po czym bieżąca pozycja rozbioru (indeks) jest ustawiana za ostatnim zinterpretowanym znakiem i zwracana jest liczba jako obiekt klasy Number.
Ta metoda nie zgłasza żadnych wyjątków. W przypadku błędu interpretacji (a w zależności od formatu - występuje on albo tylko na początku napisu, albo gdzieś dalej)  zwracana jest wartość null, bieżąca pozycja nie ulega zmianie, a  indeks błędu ustawiany jest na znaku, który spowodowłą bład. Jeżeli nie ma błędu indeks błędu ma wartość -1.
Pozycje (indeks)  - bieżący i błędu - możemy uzyskiwac od obiektu ParsePosition za pomocą metod getIndex() i getErrorIndex() oraz ustawiać za pomocą odpowiednich metod setIndex(...) i setErrorIndex(...).

Program na wydruku pokazuje przykładowe użycie tej metody parse do wyodrębnienia z pliku tekstowego wszystkich informacji zapisanych w formacie walutowym (możemy sobie wyobrażać, że jest to plik opisujący jakieś wydatki, a naszym zadaniem jest ich podsumowanie)

.import java.io.*;
import java.text.*;
import java.util.*;

public class Parse2 {

  public static void main(String[] args) {

    // Format walutowy w domyślnej lokalizacji
    // czyli w PL np. 12 zł
    NumberFormat format = NumberFormat.getCurrencyInstance();

    // Lista wartości wydatków (zapisanych w tekście pliku)
    List numList = new ArrayList();

    try {
      BufferedReader br = new BufferedReader(
                            new FileReader("testdata.txt")
                          );

      // czytanie kolejnych wierszu
      String in;
      while ((in = br.readLine()) != null) {

        int p = 0;                    // bieżący indeks rozbioru
        int last  = in.length() - 1;  // ostatni indeks w wierszu

        // Utworzenie pozycji rozbioru wiersza (od 0)
        ParsePosition ppos = new ParsePosition(0);

        // Dopóki nie dobiegliśmy do końca wiersza
        while (p <= last) {
           // Próbujemy pobrać kolejną liczbę w formacie walutowym
           Number num = format.parse(in, ppos);

           if (num == null)              // jeżeli błąd,
             p = ppos.getErrorIndex()+1; // indeks na znaku po błędzie
           else {                        // jeżeli udało się sczytać wartość
             numList.add(num);           // dodajemy ją do listy
             p =  ppos.getIndex();   // indeks na następnym znaku po
           }
           ppos.setIndex(p);         // ustawiamy następną pozycję
        }                            // od której kontynuacja rozbioru
      }
      br.close();
    } catch(Exception exc) {
        exc.printStackTrace();
        System.exit(1);
    }

    // Wypisanie i podsumowanie zapisanych w pliku wydatków
    System.out.println("Wydatki w zł:");
    double suma = 0;
    for (Iterator iter = numList.iterator(); iter.hasNext(); ) {
      Number val  = (Number) iter.next();
      System.out.println(val);
      suma += val.doubleValue();
    }
    System.out.println("Wydano w sumie: " + format.format(suma));
  }
}

Gdy użyjemy tego programu wobec  pliku, zawierającego następujący tekst:

Wydano najpierw 123 zł na 23 kilo jabłek
Kolejny wydatek objął 77,77 zł (70 litrów maślanki)
a potem jeszcze doszło 999,99 zł w 4 ratach.

to w wyniku uzyskamy:

Wydatki w zł:
123
77.77
999.99
Wydano w sumie: 1 200,76 zł


Zwrócmy uwagę: tylko liczby zapisane w formacie walutowym zostały wyodrębnione, inne liczby nie były brane pod uwagę.

Przykład ten pokazuje dużą siłe klas formatorów. Ten program zadziała bez rekompilacji przy każdej innej domyślnej lokalizacji i dla plików, w których dane zapisano w innej (właściwiej dla tej lokalizacji) walucie.

Nieco brzydkie formatowanie wyniku w powyższym przykładzie można oczywiście poprawić i to w dość prosty sposób. Oto przy formatowaniu liczb mamy swoisty odpowiednik klasy ParsePosition. Tym razem jest to klasa FieldPosition , która umożliwia uzyskiwanie informacji o początku i końcu pól sformatowanego wyniku. W przypadku prostych liczb pola oznaczają część całkowitą i część dziesiętną, co jest identyfikowane przez stałe statyczne z klasy NaumberFormat o nazwach INTEGER_FIELD i FRACTIONAL_FIELD. Użycie FieldPosition w specjalnej wersji metody format pozwala na dodatkowe formatowanie wyniku.

Zatem, aby mieć dodatkową kontrolę nad formatowaniem:
Pokazuje to poniższy program, który stanowi modyfikację poprzedniego przykładu dokonaną z myślą o wyrównywaniu wyprowadzanych wyników "na" separatorze miejsc dziesiętnych.

public class ParseAndFormat {

  public static void main(String[] args) {

    // ... tutaj część analogiczna jak w poprzednim pzrykładzie
    // ... czytanie danych z pliku i parsowanie liczb wg formatu walutowego
    // ... dla przejrzystości format ten (uzyskiwany z getCurrencyInstance())
    // ... nazwano cform (nazwa zmiennej)

    // Wypisanie i podsumowanie przeczytanych z pliku wydatków
    // formatowanie wyjścia za pomocą metody align (zob. dalej)
    // w której używamy FieldPosition

    // Format wyjściowy - walutowy, z dwoma miejscami dziesiętnymi
    DecimalFormat outform = new DecimalFormat("#.00 ¤");

    System.out.println("Wydatki w : " +
                        cform.getCurrency().getSymbol()); // symbol waluty
    double suma = 0;
    int i = 1;
    final int DOTPOS = 30; // pozycja separatora miejsc dziesiętnych

    for (Iterator iter = numList.iterator(); iter.hasNext(); i++ ) {
      Number val  = (Number) iter.next();
      suma += val.doubleValue();
      System.out.println(
        align("Pozycja " + i, outform, val, DOTPOS)
      );
    }
    System.out.println(
        align("Wydano w sumie", outform, new Double(suma), DOTPOS)
    );
  }

  // Metoda wyrównująca liczby na separatorze miejsc dziesiętnuych
  // Liczba v formatowana zgodnie z formatem f
  // poprzedzona jest napisem msg
  // i taką liczbą kropek, by separatory miejsc dziesiętnych
  // były wyrównane na pozycji width

  static StringBuffer align(String msg, Format f, Number v, int width) {

    //Interesuje nas pole - część całkowita liczby
    FieldPosition fp = new FieldPosition(NumberFormat.INTEGER_FIELD);

    // Bufor do którego zapisywana jest sformatowana liczba
    // na początku bufora już zapisujemy msg (opis pozycji wydatków)

    StringBuffer out = new StringBuffer(msg);
    int msgLen = out.length();

    // formatowanie: v = liczba, out - bufor wynikowy, fp - opis pola
    f.format(v, out, fp);

    // Po sformatowaniu metoda fp.getEndIndex() zwraca
    // pozycję końca pola = części całkowitej liczby
    // łatwo obliczyć liczbę dodatkowych (dotNum) "wypełniaczy",
    // potrzebnych, by separator dziesiętny znalazł się na pozycji width

    int dotNum = width - fp.getEndIndex();

    // Wstawiamy do bufora - jako wypełniacze - kropki
    while (dotNum-- > 0) {
      out.insert(msgLen, '.');
    }
    return out;
  }

}
Teraz wyniki przetwarzania naszego pliku tekstowego będą wyglądac następująco:

Wydatki w : zł
Pozycja 1..................123,00 zł
Pozycja 2...................77,77 zł
Pozycja 3..................999,99 zł
Wydano w sumie............1200,76 zł
Uwaga: program nie jest uniwesralny, gdyż pozycja "równania" na separatorze miejsc dziesiętnych jest niezależna od długości poprzedzającego liczbę  napisu msg. Ogólnie, trzeba by najpierw określić maksymalną długość napisów wyprowadzanych przed kolejnymi liczbami i dostosować do tego wielkość width. Pominęliśmy jednak ten etap, aby nie zaciemniać ogólnego mechanimzu użycia FieldPosition.

Istnieją też  inne sposoby formatowania liczb.
Wśród podklas klasy NumberFormat znajdziemy klasę ChoiceFormat.
Generalnie pozwala ona kojarzyć dowolne napisy z  (półotwartymi  z prawej strony) przedziałami liczb. Formatowanie za jej pomocą polega na zastąpieniu liczby, "trafiającej" w dany przedział, skojarzonym z tym przedziałem napisem.

Jedna z wersji konstruktorów klasy ChoiceFormat przyjmuje jako argumenty:
Rozmiary obu tablic muszą być równe.
Liczby w tablicy muszą być uporządkowane w kolejności rosnącej.


Reguły działania na zdefiniowanych przedziałach i kojarzenia liczb trafiających w dany przedział z napisami można opisać w następujący sposób.

Jeżeli:

double[] val = { .... }; // przedziały liczbowe
String[]  msg = { ... }; //  napisy

ChoiceFormat cf = new ChoiceFormat(val, msg);

double x = ...;
String out = cg.format(x);

to:

gdy val[0] < x, to out = msg[0],  
gdy val[0] <= x < val[1],  to out == msg[0],
gdy val[1] <= x < val[2],  to out == msg[1].
gdy val[2] <= x < val[3],  to out == msg[2].
...
gdy val[val.length-2] <= x < val[val.length-1], to out == msg[msg.length-2]
gdy val[val.length-1] <= x , to out = msg[msg.length-1] 



Pokazuje to poniższy program:
import java.text.*;

public class ChoiceForm {

  public static void main(String[] args) {
    double[] vals = {  -1, 0, 1, 10 };
    String[] msg  = { "x mniejsze od zera",
                      "0 <= x < 1",
                      "1 <= x < 10",
                      "x >= 10"
                    };
    ChoiceFormat cf = new ChoiceFormat(vals, msg);
    String in;
    while ((in = JOptionPane.showInputDialog("Podaj x")) != null) {
      double x = Double.parseDouble(in);
      String out = cf.format(x);
      System.out.println(x + " : " + out);
    }
  }

}
który może (w zależności od podawanych liczb) wyprowadzić np. taki wynik:

11.0 : x >= 10
10.0 : x >= 10
9.99999 : 1 <= x < 10
0.0 : 0 <= x < 1
1.0 : 1 <= x < 10
2.0 : 1 <= x < 10
-0.5 : x mniejsze od zera
-10000.0 : x mniejsze od zera



Korzystając z klasy ChoiceFormat czasem wygodnie będzie posłużyć się wzorcem formatowania.
Wzorzec definiujemy w postaci łańcucha znakowego (String):

liczba sep napis | liczba sep napis | liczba sep napis ....

gdzie kolejne, rozdzielone znakiem | fragmenty określają kojarzone przedziałów i napisów, a jako separatora (sep) możemy użyć znaków # lub <.

Wzorzec podajemy jako argument konstruktora klasy ChoiceFormat.

Wyjaśnia to przykładowy program.
import javax.swing.*;
import java.text.*;

public class ChoiceForm2 {

  public static void main(String[] args) {
    String pattern =
     "0#brak jablek |0<niecala polowa jablka |"+
     "0.5#rowno pol jablka | 0.5<ponad polowa jablka |" +
     "1#jedno jablko |1<niecale dwa jablka |" +
     "2#dwa jablka |2<wiecej niż dwa jablka";

    ChoiceFormat cf = new ChoiceFormat(pattern);
    String in;
    while ((in = JOptionPane.showInputDialog("Ile jest jabłek?")) != null) {
      double x = Double.parseDouble(in);
      String out = cf.format(x);
      System.out.println("Podano: " + x + " Wynik: " +  out);
    }
  }

}
a jego możliwy wynik wyhląda następująco:

Podano: 0.0 Wynik: brak jablek
Podano: 0.1 Wynik: niecala polowa jablka
Podano: 0.5 Wynik: rowno pol jablka
Podano: 0.6 Wynik: ponad polowa jablka
Podano: 1.0 Wynik: jedno jablko
Podano: 1.7 Wynik: niecale dwa jablka
Podano: 2.0 Wynik: dwa jablka
Podano: 2.1 Wynik: wiecej niż dwa jablka
Podano: 3.0 Wynik: wiecej niż dwa jablka
Podano: -1.0 Wynik: brak jablek


Klasa ChoiceFormat jest szczególnie użyteczna przy internacjonalizacji napisów w programie z wykorzystaniem klasy MessageFormat - o czym będziemy mówić dalej.

Uwaga: aby korzystać z klas pakietów ICU należy udostępnić archiwum JAR z tymi pakietami. Możemy to uczynić na kilka sposobów:
W bibliotekach ICU4J znajdziemy znacznie bardziej zaawansowany odpowiednik  klasy ChoiceFormat - klasę  RuleBasedNumberFormat.
Pozwala ona na formatowanie liczb za pomocą definiowania zestawów reguł.
Przykładowe zdefiniowane już reguły dla formatora RuleBasedNumber to:
Te reguły są już gotowe dla niektórych lokalizacji, nic nie stoi też na przeszkodzie, by tworzyć własne reguły (np. dla innych lokalizacji lub całkiem innego rodzaju).
Opis sposobów definiowania reguł zawarty jest w dokumentacji ICU4J, tu przyjrzymy się tylko przykładowym zastosowaniom gotowych reguł.

Poniższy program pokazuje najpierw słowny opis liczb w różnych lokalizacjach (niestety formator SPELLOUT nie jest spolszczony), po czym liczby porządkowe oraz czas (ale tylko po angielsku, bo formatory ORDINAL i DURATION nie są zlokalizowane dla większości innych jezyków).

import com.ibm.icu.text.*;  // podpakiet ICU - dla RuleBasedNumberFormat
import java.util.*;         // Locale
import javax.swing.*;       // JOptionPane


class RuleBasedNumberFormatTest {

  public static void main(String[] args) {

    // Lokalizacje
    Locale[] loc = { new Locale("en"),
                     new Locale("es"),
                     new Locale("de"),
                     new Locale("ru"),
                   };

    // Tablica formatorów typu SPELLOUT - dla każdej lokalizacji jeden
    RuleBasedNumberFormat[] rbnfSpell =
                            new RuleBasedNumberFormat[loc.length];

    // Utworzenie formatorów SPELLOUT
    for (int i= 0; i < loc.length; i++) {
      rbnfSpell[i] = new RuleBasedNumberFormat(
                         loc[i], RuleBasedNumberFormat.SPELLOUT
                     );
    }

    // Wartości do formatowania
    long[] values = { 5, 9, 10, 12, 20, 23, 111, 1001 };

    // Jako SPELLOUT
    show(values, rbnfSpell);

    // Jako liczby porządkowe - tylko angielskie
    show(values,
         new NumberFormat[] { new RuleBasedNumberFormat(
                                 loc[0], RuleBasedNumberFormat.ORDINAL
                              )
                            }
        );

    // Formatowanie czasów wyrażonych w sekundach
    // Tylko lokalizacja angielska
    long[] times = { 12, 800, 3712, 9600 };
    show(times,
         new NumberFormat[] { new RuleBasedNumberFormat(
                                  loc[0], RuleBasedNumberFormat.DURATION
                              )
                            }
    );
    System.exit(0);
  }

  // Ogólna metoda formatująca i pokazująca wyniki
  // Argumenty: tablica liczb do sformatowania
  //            tablica formatorów

  static void show(long[] val, NumberFormat[] rbnf) {
    String msg = "";
    for (int i=0; i < val.length; i++) {
      msg += "\n" + val[i];
      for (int j=0; j<rbnf.length; j++) {
        msg += " = " + rbnf[j].format(val[i]);
      }
    }
    JOptionPane.showMessageDialog(null, msg);
  }

}
Wyniki działania programu pokazują kolejne rysunki.

r

r r
 







Omawiane formatory potrafią wykonywać operacje odwrotne, np. przekształcać liczby wyrażone słownie na ich wartości, a przy tym możliwe jest ustawienie opcji "lenientParse", która "luźno" traktuje napisy (słowne wyrażenie liczb może być różne np. ze spacjami, z myślnikami, w ogóle bez znaków rozdizielających słowa, z różną wielkością liter itp.).

Zobaczmy przykład. Poniższy program "umie" odczytać słowne (angielskie) opisy liczb i przekształcić je w liczby.

import com.ibm.icu.text.*;  // podpakiet ICU - dla RuleBasedNumberFormat
import java.util.*;         // Locale
import java.text.*;         // ParsePosition
import javax.swing.*;       // JOptionPane


class LenientParseTest {

  public static void main(String[] args) {

    // formator typu SPELLOUT
    RuleBasedNumberFormat rbnf = new RuleBasedNumberFormat(
                                     new Locale("en"),
                                     RuleBasedNumberFormat.SPELLOUT
                                 );

    // Ustalenie "luźnego" parsowania
    rbnf.setLenientParseMode(true);

    // Wartości do parsowania
    String[] snum = { "twenty-one", "TWENTYone",
                      "one hundred and one",
                      "one-hundred-and-one",
                      "one-hundred AND one",
                    };

    // Przekształcenie opisów na liczby

    for (int i=0; i< snum.length; i++) {
      Number val = rbnf.parse(snum[i], new ParsePosition(0));
      System.out.println(snum[i] + "   =   " + val);
    }

  }

}

Wynik programu:

twenty-one   =   21
TWENTYone   =   21
one hundred and one   =   101
one-hundred-and-one   =   101
one-hundred AND one   =   101


Formator parsuje również liczby opisywane w innych popularnych językach.

1.3. Waluty

Klasa Currency z pakietu java.util opisuje waluty. Obiekty tego typu są wykorzystywane przez klasę DecimalFormat i możemy je np. stosować dla zmiany formatów walutowych (metoda setCurrency(Currency) z klasy DecimalFormat).

Klasa Currency może być użyteczna w różnych sytuacjach.
Wyobraźmy sobie taki scenariusz: mamy stworzyć aplikację, która generuje raporty o aktualnych kursach wybranych walut w kilku językach. Aktualne kursy pobieramy z jakiegoś serwisu WEB na podstawie podanych międzynarodowych symboli walut.
Aby taki program można było napisać, trzeba wiedzieć jakie są symbole walut i umieć  tłumaczyć symbole walut na wybrane języki.

Informacje, które można uzyskać o walutach przedstawia w syntetycznej postaci poniższy program.

import java.util.*;

public class Waluty {

  public static void main(String[] args) {
    // Domyślna lokalizacja (w naszym przypadku polska)
    Locale def = Locale.getDefault();

    // Tablica dostępnych lokalizacji
    Locale[] loc = Locale.getAvailableLocales();

    char[] zera = { '0', '0', '0', '0', '0', '0' }; // do pokazu walut

    for (int i=0; i<loc.length; i++) {
      String countryCode = loc[i].getCountry();  // kod kraju
      String langCode = loc[i].getLanguage();    // kod języka
      if (countryCode.equals("")) continue;

      // lokalizacja opisana w języku domyślnej lokalizacji (polskim)
      String kraj =  loc[i].getDisplayCountry(def);
      String jezyk = loc[i].getDisplayLanguage(def);

      // Waluta dla lokalizacji loc[i]
      Currency c = Currency.getInstance(loc[i]);
      String sym  =  c.getSymbol();      // symbol w domyślnej lokalizacji (pl)
      String nsym = c.getSymbol(loc[i]); // symbol w danym kraju
      String icode = c.getCurrencyCode();     // międzynarod. kod waluty
      int cdig = c.getDefaultFractionDigits(); // ile może być miejsc dzies.

      System.out.println(loc[i]+" kraj: "+kraj+" język: "+jezyk +
        "\nwaluta: "+sym+" "+nsym+" "+icode +
                   " grosz = 1/1"+new String(zera,0,cdig)+" "+sym +
        "\n============================================================"
        );
     }
  }

}
Warto zwrócić uwagę na informację jaka jest minimalna cześć podstawowej jednostki pieniężnej (tu nazwaliśmy to "groszem"), uzyskiwaną za pomocą metody getFractionDigits().
 
Fragment wydruku działania programu przedstawiono poniżej:

es_VE kraj: Wenezuela język: hiszpański
waluta: VEB Bs VEB grosz = 1/100 VEB
============================================================
et_EE kraj: Estonia język: estoński
waluta: EEK kr EEK grosz = 1/100 EEK
============================================================
fi_FI kraj: Finlandia język: fiński
waluta: EUR ? EUR grosz = 1/100 EUR
============================================================
fr_BE kraj: Belgia język: francuski
waluta: EUR ? EUR grosz = 1/100 EUR
============================================================
fr_CA kraj: Kanada język: francuski
waluta: CAD $ CAD grosz = 1/100 CAD
============================================================
fr_CH kraj: Szwajcaria język: francuski
waluta: CHF SFr. CHF grosz = 1/100 CHF
============================================================
hu_HU kraj: Węgry język: węgierski
waluta: HUF Ft HUF grosz = 1/100 HUF
============================================================
is_IS kraj: Islandia język: islandzki
waluta: ISK kr. ISK grosz = 1/100 ISK
============================================================
ja_JP kraj: Japonia język: japoński
waluta: JPY ? JPY grosz = 1/1 JPY
============================================================
ko_KR kraj: Korea Południowa język: koreański
waluta: KRW ? KRW grosz = 1/1 KRW
============================================================
pl_PL kraj: Polska język: polski
waluta: zł zł PLN grosz = 1/100 zł
============================================================
pt_BR kraj: Brazylia język: portugalski
waluta: BRL R$ BRL grosz = 1/100 BRL
============================================================
pt_PT kraj: Portugalia język: portugalski
waluta: EUR ? EUR grosz = 1/100 EUR


Pakiet ICU dostarcza dodatkowych możliwości, gdy chodzi o waluty. M.in. możemy uzyskać bardziej opisową, stosowaną w podanej lokalizacji nazwę waluty. Pokazuje to poniższy program.
import javax.swing.*;
import java.awt.*;
import java.util.Locale;
import com.ibm.icu.util.Currency;


public class Waluty1 {

  public static void main(String[] args) {

    Locale def = Locale.getDefault();
    Locale en =  new Locale("en", "US");


    // Tablica dostępnych lokalizacji
    Locale[] loc = Currency.getAvailableLocales();


    boolean[] b  = { false }; // dla getName() - oznacza normalny wynik
                              // w przeciwieństwie do wyniku dla ChoiceFormat

    String out = "";

    for (int i=0; i<loc.length; i++) {
      String kraj =  loc[i].getDisplayCountry(def);
      if (kraj.equals("")) continue;
      String lang = loc[i].getDisplayLanguage(def);
      String variant = loc[i].getVariant();

      // Waluta dla lokalizacji loc[i]
      Currency c = Currency.getInstance(loc[i]);

      String icode = c.getCurrencyCode();     // międzynarod. kod waluty

      // symbol w danym kraju
      String sym  =  c.getName(loc[i], Currency.SYMBOL_NAME, b);
      // nazwa w danym kraju
      String name = c.getName(loc[i], Currency.LONG_NAME, b);
      // nazwa po angielsku
      String enName = c.getName(en, Currency.LONG_NAME, b);


      out += kraj + " " + lang + " " + variant +
        "\nwaluta: "+icode+" "+sym+
        "\n"+ name + '\n' + enName +
        "\n------------------------------------------\n";
     }

     JFrame f = new JFrame();
     JTextArea ta = new JTextArea();
     ta.setFont(new Font("Dialog", Font.BOLD, 14));
     ta.setText(out);
     f.getContentPane().add(new JScrollPane(ta));
     f.pack();
     f.show();
  }

}


Uwaga: aby móc bez problemów odczytywać znaki Unikodu wyniki pokazujemy w polu tekstowym JTextArea (o programowaniu graficznych interfejsów użytkownika zob. część D książki)
Fragmenty wyników działania programu prezentuje poniższy rysunek.

r



1.4. Strefy czasowe

Strefy czasowe są przedstawiane przez obiekty klasy TimeZone z pakietu java.util.
Aby uzyskać aktualną, domyślną dla komputera na którym działa nasz program, strefę czasową stosujemy statyczną metodę getDefault() z klasy TimeZone.
O strefie czasowej możemy dowiedzieć się wielu interesujących informacji, co pokazuje poniższy program.
import java.util.*;

public class Strefy0 {

  public static void main(String[] args) {
    TimeZone tz = TimeZone.getDefault();

    // --- Informacje o strefie czasowej

    // identyfikator strefy
    String id = tz.getID();

    // różnica względem czasu standardowego (UCT)
    int diff = tz.getRawOffset();

    // czy strefa używa czasu letniego
    boolean useDST = tz.useDaylightTime();

    // ile czasu trzeba dodać do lokalnego zegara
    // aby uzyskać czas bez ew. przesunięcia letniego
    int dstSav = tz.getDSTSavings();

    // Nazwa strefy
    String defName = tz.getDisplayName();

    // Krótka nazwa strefy
    String shortName = tz.getDisplayName(useDST, TimeZone.SHORT);

    // Nazwa strefy w podanym języku
    String locName = tz.getDisplayName(new Locale("fr"));

    // Dluga nazwa strefy
    String fullName = tz.getDisplayName(useDST, TimeZone.LONG);

    // Długa nazwa strefy w podanym języku
    String locFullName = tz.getDisplayName(useDST, TimeZone.LONG,
                         new Locale("es"));

    // Jaka jest aktualna różnica czasu TERAZ wobec UCT,
    // z uwzględnieniem czasu letniego
    Date teraz = new Date();
    long ms = teraz.getTime();
    int offset = tz.getOffset(ms);


    System.out.println("ID = " + id);
    System.out.println("RawOffset = " + diff);
    System.out.println("useDaylightTime = " + useDST);
    System.out.println("DSTSavings = " + dstSav);
    System.out.println("DisplayName = " + defName);
    System.out.println("DisplayName short = " + shortName);
    System.out.println("DisplayName full = " + fullName);
    System.out.println("DisplayName locale(\"fr\") = " + locName);
    System.out.println("DisplayName full locale(\"es\") = " + locFullName);
    System.out.println("Offset = " + offset);
  }

}
i jego wyniki:

ID = Europe/Belgrade
RawOffset = 3600000
useDaylightTime = true
DSTSavings = 3600000
DisplayName = Central European Time
DisplayName short = CEST
DisplayName full = Central European Summer Time
DisplayName locale("fr") = Heure d'Europe centrale
DisplayName full locale("es") = Hora de verano de Europa Central
Offset = 7200000


Uwagi:
W naszym programie możemy skonstruować dowolną strefę czasową, używając metody TimeZone.getTimeZone(String ID) i podając jako argument identyfikator strefy czasowej.
Listę dostępnych identyfikatorów można uzyskać jako tablicę Stringów za pomoca odwołania TimeZone.getAvailableIDs().

Poniższy przykładowy programik pokazuje jak można wyliczyć aktualną różnicę czasu pomiędzy podanymi strefami czasowymi oraz jak można dowiedzieć się jakie strefy czasowe mają podaną różnicę czasu wobec GMT.

import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class Strefy1 {

  public static void main(String[] args) {

    // Konstruowanie stref czasowych
    TimeZone myTz = TimeZone.getTimeZone("Europe/Warsaw");
    TimeZone java = TimeZone.getTimeZone("Asia/Jakarta");
    TimeZone cuba = TimeZone.getTimeZone("America/Havana");

    // za pomocą pokazanej dalej metody getDiffMsg
    // wyliczamy i pokazujemy aktualną różnicę czasu
    // pomiędzy sterfami czasowymi

    System.out.println(getDiffMsg(myTz, java));
    System.out.println("--------------------------------------------------");
    System.out.println(getDiffMsg(myTz, cuba));
    System.out.println("--------------------------------------------------");
    System.out.println(getDiffMsg(cuba, java));
    System.out.println("--------------------------------------------------");

    // Jakie strefy czasowe mają podaną różnicę czasu wobec GMT

   for (int k = 12; k <= 14; k++) {
     String[] ids = TimeZone.getAvailableIDs(k*3600000);
     Arrays.sort(ids);
     System.out.println(
      "Strefy czasowe mające różnice +" + k + " godzin wobec GMT" );
     for (int i=0; i < ids.length; i++) {
       System.out.println(ids[i]);
     }
     System.out.println("--------------------------------------------------");
   }
  }

  static String getDiffMsg(TimeZone z1, TimeZone z2) {
    Date data = new Date();
    long teraz = data.getTime();
    double offset1 = z1.getOffset(teraz)/3600000.0;
    double offset2 = z2.getOffset(teraz)/3600000.0;
    double diff;
    if (offset1 > offset2)  diff = -(offset1 - offset2);
    else diff = offset2 - offset1;
    String out =  "Różnica czasu pomiędzy" + '\n' +
                  z1.getID() + " i " + z2.getID()  + '\n' +
                  "wynosi teraz : " + diff + " godz."  + '\n' +
                  "W strefie " + z1.getID() +
                       (z1.inDaylightTime(data) ? " " : " nie ") +
                       "działa czas letni"  + '\n' +
                  "W strefie " + z2.getID() +
                       (z2.inDaylightTime(data) ? " " : " nie ") +
                       "działa czas letni";

    return out;
  }

}
Wydruk:

Różnica czasu pomiędzy
Europe/Warsaw i Asia/Jakarta
wynosi teraz : 5.0 godz.
W strefie Europe/Warsaw działa czas letni
W strefie Asia/Jakarta nie działa czas letni
--------------------------------------------------
Różnica czasu pomiędzy
Europe/Warsaw i America/Havana
wynosi teraz : -6.0 godz.
W strefie Europe/Warsaw działa czas letni
W strefie America/Havana działa czas letni
--------------------------------------------------
Różnica czasu pomiędzy
America/Havana i Asia/Jakarta
wynosi teraz : 11.0 godz.
W strefie America/Havana działa czas letni
W strefie Asia/Jakarta nie działa czas letni
--------------------------------------------------
Strefy czasowe mające różnice +12 godzin wobec GMT
Antarctica/McMurdo
Antarctica/South_Pole
Asia/Anadyr
Asia/Kamchatka
Etc/GMT-12
Kwajalein
NST
NZ
Pacific/Auckland
Pacific/Fiji
Pacific/Funafuti
Pacific/Kwajalein
Pacific/Majuro
Pacific/Nauru
Pacific/Tarawa
Pacific/Wake
Pacific/Wallis
--------------------------------------------------
Strefy czasowe mające różnice +13 godzin wobec GMT
Etc/GMT-13
Pacific/Enderbury
Pacific/Tongatapu
--------------------------------------------------
Strefy czasowe mające różnice +14 godzin wobec GMT
Etc/GMT-14
Pacific/Kiritimati
--------------------------------------------------


1.5. Kalendarze

Informacje o datach i czasie są w Javie reprezentowane przez obiekty klasy Calendar.

Informacje o bieżącej dacie i czasie możemy uzyskać m.in. za pomocą odwołania:

        Calendar c = Calendar.getInstance();

które zwraca obiekt - domyślny kalendarz dla domyślnej lokalizacji ustawiony na bieżącą datę i czas w strefie czasowej właściwej dla domyślnej lokalizacji.


Informacje o dacie i czasie są zapisane w polach obiektu-kalendarza. Dostęp do tych pól uzyskujemy za pomocą metody get(...) , użytej na rzecz obiektu-kalendarza, z argumentem - stałą statyczną klasy Calendar, okreslającą o jaki rodzaj informacji nam chodzi. Oprócz tego pewne informacje, związane z właściwościami danego kalendarza lub dla danej lokalizacji można uzyskać za pomocą innych metod get... (np. jaki jest pierwszy dzień tygodnia - niedziela czy poniedziałek - getFirstDayOfWeek()).

Przykładowy program spełnia funkcję przewodnika po polach kalendarza,  pokazują ich znaczenie oraz sposoby uzyskiwania ich wartości.

import java.util.*;

public class Kal1 {

  public static void say(String s) { System.out.println(s+'\n'); }

  public static void main(String[] args) {

    // uzyskanie kalendarza domyślnego
    // (obowiązującgo dla domyślnej lokalizacji - tu dla Polski)
    // ustawionego na bieżącą datę i czas

    Calendar cal = Calendar.getInstance();

    say("ERA.............. " + cal.get(Calendar.ERA) +
        " (tu: 0=pne, 1=AD)");

    say("ROK.............. " + cal.get(Calendar.YEAR));
    say("MIESIĄC.......... " + cal.get(Calendar.MONTH) +
        " (0-styczeń, 2-luty, ..., 11-grudzień)");

    say("LICZBA DNI\n" +
        "W MIESIĄCU....... " + cal.getActualMaximum(Calendar.DAY_OF_MONTH));

    say("DZIEŃ MIESIĄCA... " + cal.get(Calendar.DAY_OF_MONTH));
    say("DZIEŃ MIESIĄCA... " + cal.get(Calendar.DATE));
    say("TYDZIEŃ ROKU..... " + cal.get(Calendar.WEEK_OF_YEAR));
    say("TYDZIEŃ MIESIĄCA. " + cal.get(Calendar.WEEK_OF_MONTH));
    say("DZIEŃ W ROKU..... " + cal.get(Calendar.DAY_OF_YEAR));

    say("PIERWSZY DZIEŃ\n" +
        "TYGODNIA......... " + cal.getFirstDayOfWeek() +
        " (1-niedziela, 2-poniedziałek, ..., 7 sobota)");

    say("DZIEŃ TYGODNIA... " + cal.get(Calendar.DAY_OF_WEEK) +
        " (1-niedziela, 2-poniedziałek, ..., 7-sobota)");

    say("GODZINA.......... " + cal.get(Calendar.HOUR) +
        " (12 godzinna skala; następne odwolanie czy AM czy PM)");

    say("AM/PM............ " + cal.get(Calendar.AM_PM) +
        " (AM=0, PM=1)");

    say("GODZINA.......... " + cal.get(Calendar.HOUR_OF_DAY) +
        " (24 godzinna skala)");

    say("MINUTA........... " + cal.get(Calendar.MINUTE));
    say("SEKUNDA.........  " + cal.get(Calendar.SECOND));
    say("MILISEKUNDA:      " + cal.get(Calendar.MILLISECOND));

    int msh = 3600*1000; // liczba milisekund w godzinie

    say("RÓŻNICA CZASU\n" +
        "WOBEC GMT........ " + cal.get(Calendar.ZONE_OFFSET)/msh);

    say("PRZESUNIĘCIE\n" +
        "CZASU............ " + cal.get(Calendar.DST_OFFSET)/msh +
        " (w Polsce obowiązuje w lecie)");

  }

}
Na wydruku pokazano wyniki działania programu, uruchomionego we wtorek 6 maja 2003 roku o godzinie 18:05:00.
Wydruk:
ERA.............. 1 (tu: 0=pne, 1=AD)

ROK.............. 2003

MIESIĄC.......... 4 (0-styczeń, 2-luty, ..., 11-grudzień)

LICZBA DNI
W MIESIĄCU....... 31

DZIEŃ MIESIĄCA... 6

DZIEŃ MIESIĄCA... 6

TYDZIEŃ ROKU..... 19

TYDZIEŃ MIESIĄCA. 2

DZIEŃ W ROKU..... 126

PIERWSZY DZIEŃ
TYGODNIA......... 2 (1-niedziela, 2-poniedziałek, ..., 7 sobota)

DZIEŃ TYGODNIA... 3 (1-niedziela, 2-poniedziałek, ..., 7-sobota)

GODZINA.......... 6 (12 godzinna skala; następne odwolanie czy AM czy PM)

AM/PM............ 1 (AM=0, PM=1)

GODZINA.......... 18 (24 godzinna skala)

MINUTA........... 5

SEKUNDA.........  0

MILISEKUNDA:      550

RÓŻNICA CZASU
WOBEC GMT........ 1

PRZESUNIĘCIE
CZASU............ 1 (w Polsce obowiązuje w lecie)

Uwaga: należy zwrócić baczną uwagę na to, że indeksowanie miesięcy rozpoczyna się od 0, a nie od 1 (czyli styczeń ma numer 0). Jest to fatalny błąd, który popełniono w pierwszej wersji Javy, wprowadzając klasę Date. Twórcy klasy Calendar (firma Taligent) mieli tego świadomość, ale - by nie wprowadzać jeszcze większego zamieszania - pozostawili tę bardzo mylącą konwencję.

Za pomocą metod set... kalendarza możemy ustawiać jego bieżącą datę i czas.
Np. aby ustawić kalendarz na 7 maja 2003 roku na tę samą godzinę co "teraz" możemy napisać:

    Calendar c = Calendar.getInstance();
    c.set(2003, 4, 7);  // rok 2003, indeks miesiąca = 4 (maj), dzień 7

a jeśli chcemy zarazem ustalić godzinę 18 minut 05 napiszemy:

    c.set(2003, 4, 7, 18, 5);

Możemy też zmieniać (ustawiać) wartości poszczególnych pól.
Służą do tego metody, które wykonują operacje na datach.

Operacje na datach wykonujemy za pomocą następujących metod:

        set(id_pola,  wartość)
        add(id_pola, wartość)
        roll(id_pola, wartość)

    gdzie:
        id_pola - stała statyczna z klasy Calendar, określająca pole na którym wykonywana jest oparacja,
        wartość - nowa wartość pola.


Wszystkie w/w operacje uwzględniają reguły danego kalendarza, a różnica pomiędzy nimi jest następująca:
Dokładne reguły obliczeniowe są podane w dokumentacji klasy Calendar.
Przykładowy program na wydruku testuje działanie omówionych metod.

import java.util.*;
import javax.swing.*;

public class TestKal {

  public static void main(String[] args) {
    String in;
    int d = 0;
    while ((in = JOptionPane.showInputDialog("DATE:")) != null) {
      d = Integer.parseInt(in);
      show("set",  "DATE", Calendar.DATE, d);
      show("add",  "DATE", Calendar.DATE, d);
      show("roll", "DATE", Calendar.DATE, d);
    }
    System.exit(0);
  }

  static void say(String s) { System.out.println(s); }


  static void show(String oper, String what, int field, int value) {
    Calendar c = Calendar.getInstance();
    say("Teraz jest: " + c.getTime());
    say("Operacja: " + oper + "(Calendar." + what + ", " + value + ")");
    if (oper.equals("set")) c.set(field, value);
    else if (oper.equals("add")) c.add(field, value);
         else if (oper.equals("roll")) c.roll(field,value);

    say("Aktualne ustawienia kalendarza: " + c.get(Calendar.YEAR) + '/'
                               + (c.get(Calendar.MONTH) + 1) + '/' +
                               + c.get(Calendar.DATE));
     say("-----------------------------------------------------------");
  }

}
a wydruk jego dzialania (dla kilku wprowadzonych wartości pola Calendar.DATE, okreslającego dzień miesiąca) ilustruje działanie metod set, add i roll oraz róznice między nimi:

Teraz jest: Mon May 05 07:27:43 CEST 2003
Operacja: set(Calendar.DATE, 7)
Aktualne ustawienia kalendarza: 2003/5/7
-----------------------------------------------------------
Teraz jest: Mon May 05 07:27:43 CEST 2003
Operacja: add(Calendar.DATE, 7)
Aktualne ustawienia kalendarza: 2003/5/12
-----------------------------------------------------------
Teraz jest: Mon May 05 07:27:43 CEST 2003
Operacja: roll(Calendar.DATE, 7)
Aktualne ustawienia kalendarza: 2003/5/12
-----------------------------------------------------------
Teraz jest: Mon May 05 07:27:55 CEST 2003
Operacja: set(Calendar.DATE, 31)
Aktualne ustawienia kalendarza: 2003/5/31
-----------------------------------------------------------
Teraz jest: Mon May 05 07:27:55 CEST 2003
Operacja: add(Calendar.DATE, 31)
Aktualne ustawienia kalendarza: 2003/6/5
-----------------------------------------------------------
Teraz jest: Mon May 05 07:27:55 CEST 2003
Operacja: roll(Calendar.DATE, 31)
Aktualne ustawienia kalendarza: 2003/5/5
-----------------------------------------------------------
Teraz jest: Mon May 05 07:27:58 CEST 2003
Operacja: set(Calendar.DATE, 32)
Aktualne ustawienia kalendarza: 2003/6/1
-----------------------------------------------------------
Teraz jest: Mon May 05 07:27:58 CEST 2003
Operacja: add(Calendar.DATE, 32)
Aktualne ustawienia kalendarza: 2003/6/6
-----------------------------------------------------------
Teraz jest: Mon May 05 07:27:58 CEST 2003
Operacja: roll(Calendar.DATE, 32)
Aktualne ustawienia kalendarza: 2003/5/6

Uwaga: CEST oznacza Central European Summer Time

Zwróćmy uwagę na metodę getTime(). Wykorzystano ją w omawianym programie, by łatwo wypisać datę i czas w jakiejś ludzkiej postaci,
Metody zdezaktualizowane (deprecated) są utrzymywane w standardowych pakietach Javy ze względu na aplikacje, które zostały kiedyś napisane i odwołują sie do tych metod. Pisząc nowe programy nie nalezy używac takich metod, nie tylko dlatego, że istnieją inne, nowsze i lepsze metody, ale również dlatego, że użycie metod zdezaktualizowanych w niektórych okolicnościach może prowadzić do błędnego działania aplikacji.
Metoda ta zwraca obiekt klasy Date, reprezentujący datę i czas dla danego ustawienia kalendarza. Klasa ta w dawnych wersjach Javy była stosowana do dzialania na datach i czasie, ale ponieważ nie była ona zlokalizowana ani nie uwzględniała możliwośc istnienia różnych kalendarzy, to w tej chwili większość jej metod  jest zdezaktualizowana i zamiast nich należy stosować metody klasy Calendar.
Jednak klasa Date nadal jest  przydatna przy:
Porównywania dat i czasu możemy również łatwo dokonywać za pomocą metody getTimeInMillis() z klasy Calendar, która zwraca liczbę milisekund, które (dla danego ustawienia kalendarza) uplynęły od 1 stycznia 1970 roku (taka szczególna data, od której odlicza się czas nazywa się datą początku epoki).
Warto zwrócić przy tym uwagę, że wewnętrznie klasa kalendarza prowadzi równoległą podwójną reprezentację daty i czasu - za pomocą omówionych wcześniej pól oraz za pomocą  liczby milisekund od początku epoki  (wartość typu long, ją własnie zwraca metoda getTimeInMillis()). Wartość milisekund można ustawiać za pomocą metody setTimeInMillis(long) klasy Calendar.

Mówiliśmy już o tym, że Calendar.getInstance() zwraca domyślny kalendarz dla domyślnej lokalizacji i związanej z nią strefy czasowej.
Możemy też uzyskać inne kalendarze:
Oto prosty przykłady.
W poniższym fragmencie kodu:
    TimeZone tz = TimeZone.getTimeZone("Asia/Jakarta");
    Calendar c = Calendar.getInstance(tz);
    System.out.println("Current time: " + c.getTime());
    System.out.println("Java time: " +
         c.get(Calendar.HOUR_OF_DAY) + ":" + c.get(Calendar.MINUTE));

kalendarz ustawiany jest na strefę czasową Javy (wyspy, nie języka). Metoda getTime() zwróći  aktualny czas w domyślnej lokalizacji, ale pola kalendarza są ustawiane z uwzględnieniem różnicy czasu.
W wyniku otrzymamy.
Current time: Fri Jul 18 12:44:47 CEST 2003
Java time: 17:44


Czym różnią się kalendarze dla różnych lokalizacji?
Przede wszystkim rodzajem kalendarza. Klasa Calendar jest klasą abstrakcyjną. Konkretne klasy kalendarzy są jej podklasami. Najczęściej spotkamy się z klasą GregorianCalendar (która reprezentuje kalendarz gregoriański).
Ale nawet w standardzie Javy znajdziemy też inny kalendarz - mianowicie buddyjski - właściwy np. dla lokalizacji tajskiej. Zobaczmy.
Następujący fragment kodu:
    Calendar c = Calendar.getInstance();
    System.out.println(c.getClass().getName());
    c = Calendar.getInstance(new Locale("th", "TH"));
    System.out.println(c.getClass().getName());
wyprowadzi:
java.util.GregorianCalendar
sun.util.BuddhistCalendar


Dużo więcej kalendarzy znajdziemy w pakiecie ICU.
Mamy tam kalendarze: buddyjski, tradycyjny chiński, tradycyjny japoński, islamski, hebrajski.

Sposób użycia tych kalendarzy oraz tóżnice pomiędzy nimi pokazuje poniższy program.
import com.ibm.icu.util.*;
import com.ibm.icu.text.*;

public class MiscCal {

  public static void main(String[] args) {
    Calendar[] kal = {
               Calendar.getInstance(),   // domyślny kalendarz - gregoriański
               new GregorianCalendar(),  // jeszcze raz - ale inaczej tworzony
               new BuddhistCalendar(),   // buddyjski
               new ChineseCalendar(),    // chiński
               new JapaneseCalendar(),   // japoński
               new IslamicCalendar(),    // islamski
               new HebrewCalendar(),     // hebrajski
               };
    java.util.Date teraz = new java.util.Date(); // aktualny czas
    System.out.println("Teraz jest: " + teraz);  // po angielsku

    // przebiegamy po klaendarzach
    // ustawiamy je na bieżący czas
    // i pokazujemy wartości takich pól jak rok, miesiąc itp.

    for (int i=0; i<kal.length; i++) {
      kal[i].setTime(teraz);
      String className = kal[i].getClass().getName();
      String name = className.substring(className.lastIndexOf(".") + 1);
      System.out.println(name + " - " +
            "era " + kal[i].get(Calendar.ERA) +
            "; rok " + kal[i].get(Calendar.YEAR) +
            (name.equals("ChineseCalendar") ?
                  " czyli " + kal[i].get(Calendar.EXTENDED_YEAR)  : "") +
            "; mies " + kal[i].get(Calendar.MONTH) +
            "; dzień mies. " + kal[i].get(Calendar.DAY_OF_MONTH) +
            "; dzień tyg. " + kal[i].get(Calendar.DAY_OF_WEEK)
            );
    }
  }

}
Program wyprowadzi następujące wyniki.
Teraz jest: Fri Jul 18 15:02:24 CEST 2003
GregorianCalendar - era 1; rok 2003; mies 6; dzień mies. 18; dzień tyg. 6
GregorianCalendar - era 1; rok 2003; mies 6; dzień mies. 18; dzień tyg. 6
BuddhistCalendar - era 0; rok 2546; mies 6; dzień mies. 18; dzień tyg. 6
ChineseCalendar - era 78; rok 20 czyli 4640; mies 5; dzień mies. 19; dzień tyg. 6
JapaneseCalendar - era 235; rok 15; mies 6; dzień mies. 18; dzień tyg. 6
IslamicCalendar - era 0; rok 1424; mies 4; dzień mies. 18; dzień tyg. 6
HebrewCalendar - era 0; rok 5763; mies 10; dzień mies. 18; dzień tyg. 6


Kalendarze różnią się przede wszystkim erą i  rokiem. W buddyjskim lata liczone są od narodzin Buddy. W tradycyjnym japońskim zliczanie czasu odbywa się w ten sposób, że panowanie każdego Cesarza oznacza nową erę, a rok intronizacji jest pierwszym rokiem tej ery. Tradycyjny chiński kalendarz jest kalendarzem księżycowym (co od razu widać w rachubie miesięcy). Lata są liczone na dwa różne sposoby. Pierwszy polega na numerowaniu lat kolejno poczynając od 61 roku panowania cesarza Szy Huang Ti  (2637 r. pne). Drugi sposób bierze za początek kalendarza tę sama datę, ale używa 60- letnich cykli, w każdym z których lata numerowane są od początku. Pole  ERA chińskiego kalendarza pokazuje numer cyklu, pole YEAR - numer roku w danym cyklu (od 1 do 60), a pole EXTENDED_YEAR aktualny numer roku, liczony sekwencyjnie według pierwszego sposobu. Kalendarz hebrajski jest kalendarzem lunisolarnym (zatem zliczanie miesięcy ma ciekawe, różne od kalendarza gergoriańskiego, własciwości). Kalendarz islamski rozpoczyna rachubę lat od daty pielgrzymki Mohameta do Medyny (czwartek, 15 lipca 622 roku ).
Oczywiście, nie sposób tutaj wdawać sie w dalszą dyskusję tych zagadnień (zainteresowanych odsyłam do bogatej literatury, dotyczącej różnych kalendarzy, dostępnej również w Internecie).
Zauważmy tylko, że istnieje jeszcze wiele innych kalendarzy (do ciekawszych, a zarazem bardzo skomplikowanych nalezy np. kalendarz balijski, obowiązujący tradycyjnei na wyspie Bali). Klasa Calendar (szczególnie w wersji pakietu ICU, gdzie pod kątem ułatwień dziedziczenia jej konstrukcja została mocno przebudowana) jest przygotowana do tego, by w miarę prosto można było tworzyć jej podklasy, reprezentujące konkretne kalendarze,

Dostosowanie kalendarza do lokalizacji nie polega tylko na zmianie samego kalendarza. Ten sam kalendarz - np. gregoriański - w różnych lokalizacjach może się różnić np. pierwszym dniem tygodnia. W Polsce pierwszym dniem tygodnia jest poniedziałek (indeks 2).  Dla innych krajów - może być to inny dzień tygodnia.
Poniższy program pokazuje wszystkie kraje dla których pierwszy dzień tygodnia nie jest poniedziałkiem.

import com.ibm.icu.util.Calendar;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.Iterator;
import java.text.DateFormatSymbols;

public class FirstDoW {

  public static void main(String[] args) {
    // Domyślna lokalizacja (w naszym przypadku polska
    Locale def = Locale.getDefault();

    // Chcemy mieć dni tygodnia po polsku
    DateFormatSymbols dafs = new DateFormatSymbols(def);
    String[] wdays = dafs.getWeekdays();  // dni tygodnia po polsku


    // Tablica dostępnych lokalizacji (z pakietu ICU!!!)
    Locale[] loc = Calendar.getAvailableLocales();

    // Mapa: klucz = kraj, wartość = piewrwszy dzień tygodnia
    Map fdowMap = new TreeMap();

    for (int i=0; i<loc.length; i++) {

      // Uzyskanie kalendarza dla danej lokalizacji
      // (będą to kalendzrae gregoriańskie, bo taka jest właściwość pakietu ICU)
      Calendar c = Calendar.getInstance(loc[i]);

      // indeks pierwszego dnia tygodnia w tym kalendarzu
      int fdow = c.getFirstDayOfWeek();
      // jeżeli to poniedziałek - nie interesuej nas
      if (fdow == 2) continue;

      // lokalizacja opisana w języku domyślnej lokalizacji (polskim)
      String country = loc[i].getDisplayCountry(def);
      if (country.equals("")) continue; // pomijamy te bez kraju

      if (!fdowMap.containsKey(country)) fdowMap.put(country, wdays[fdow]);
    }

    System.out.println("Gdzie pierwszy dzień tygodnia nie jest poniedziałkiem?");
    for (Iterator it = fdowMap.keySet().iterator(); it.hasNext(); ) {
      // Jaki jest pierwszy dzień tygodnia
      String country = (String) it.next();
      String fday = (String) fdowMap.get(country);
      System.out.println(country  +  " = " + fday);
    }
  }
Wydruk programu:
Gdzie pierwszy dzień tygodnia nie jest poniedziałkiem?
Albania = niedziela
Algeria = sobota
Arabia Saudyjska = sobota
Armenia = niedziela
Australia = niedziela
Bahrajn = sobota
Botswana = niedziela
Chiny = niedziela
Chorwacja = niedziela
Dziewicze Wyspy Stanów Zjednoczonych = niedziela
Dżibuti = sobota
Egipt = sobota
Erytrea = sobota
Etiopia = sobota
FO = niedziela
Filipiny = niedziela
GL = niedziela
Hong Kong = niedziela
Indie = sobota
Irak = sobota
Iran = sobota
Islandia = niedziela
Izrael = niedziela
Japonia = niedziela
Jemen = sobota
Jordan = sobota
Jugosławia = niedziela
Kanada = niedziela
Katar = sobota
Kenia = sobota
Korea Południowa = niedziela
Kuwejt = sobota
Liban = sobota
Libia = sobota
MO = niedziela
Macedonia = niedziela
Malta = niedziela
Maroko = sobota
Nowa Zelandia = niedziela
Oman = sobota
Republika Czeska = niedziela
Republika Południowej Afryki = niedziela
Rumunia = niedziela
Singapur = niedziela
Somalia = sobota
Stany Zjednoczone Ameryki = niedziela
Sudan = sobota
Syria = czwartek
Słowacja = niedziela
Słowenia = niedziela
Tajlandia = niedziela
Tajwan = niedziela
Tanzania = sobota
Tunezja = sobota
Wietnam = niedziela
Zimbabwe = niedziela
Zjednoczone Emiraty Arabskie = sobota


Już w tym przykładzie natknęliśmy się na kwestie pokazywania dat według reguł danej lokalizacji (tu chodziło o nazwy dni tygodnia). Skorzystaliśmy przy tym z klasy DateFormatSymbols, jej obiektu utworzonego dla domyślnej lokalizacji (polskiej) i metody getWeekDays(), która - wołana na jego rzecz - zwraca nazwy dni tygodnia w języku danej lokalizacji.
Ogólnie, wyprowadzanie dat i czasu w przyjaznej dla użytkownika postaci wymaga ich formatowania za pomoca klasy DateFormat, która  posługuje się klasą DateFormatSymbols. Klasa DateFormat służy również do przekształacania tekstów (napisów) reprezentujących daty na obiekty klasy Date.

1.6. Formatowanie dat

Przy formatowaniu dat podobnie jak w przypadku liczb musimy najpierw uzyskać odpowiedni formator za  pomocą statycznych metod getXXXInstance(...) z klasy DateFormat, a następnie na jego rzecz użyć metody format z argumentem typu Date.

Możemy zastosować:
Argumenty w/w metod określają lokalizację oraz styl formatowania .
Mamy do dyspozycji cztery style formatowania określone przez statyczne stałe klasy DateFormat: SHORT, MEDIUM, LONG, FULL, a ew. dodatkowy argument metod getXXXInstance(...) określa lokalizację za pomocą referencji do obiektu klasy Locale.

Oprócz tego możemy posłużyć się wzorcami formatowania.

Najprostsze sposoby formatowania dla domyślnej i kilku wybranych lokalizacji pokazuje poniższy kod.
import java.util.*;
import java.text.*;
import javax.swing.*;

public class Daty {

  public static void main(String[] args) {
    Date teraz = new Date();

    int[] styles = { DateFormat.SHORT, DateFormat.MEDIUM,
                     DateFormat.LONG, DateFormat.FULL };

    String outMsg = "";

    for (int i=0; i < styles.length; i++) {

      DateFormat df = DateFormat.getDateTimeInstance(
                                    styles[i],          // styl daty
                                    DateFormat.FULL     // styl czasu
                      );

      outMsg += df.format(teraz) + '\n' + "-----------------\n" ;
    }
    System.out.println(outMsg);


    Locale[] llist = { new Locale("de"),
                       new Locale("fr"),
                       new Locale("es"),
                       new Locale("ar"),
                       new Locale("ru"),
                       new Locale("th")
                     };
    outMsg = "";
    for (int i=0; i < llist.length; i++) {
      DateFormat df = DateFormat.getDateTimeInstance(
                                    DateFormat.FULL,    // styl daty
                                    DateFormat.FULL,    // styl czasu
                                    llist[i]            // lokalizacja
                      );
      outMsg += df.format(teraz) + '\n' + "-----------------\n" ;
    }
    JOptionPane.showMessageDialog(null, outMsg);
    System.exit(0);
  }

}
Program wypisze na konsoli daty w domyślnej lokalizacji (pl) i wszystkich stylach oraz czas w stylu FULL. W pokazanym okienku komunikatów zobaczymy tę samą informację sformatowaną dla killku innych lokalizacjach (styl daty i czasy FULL).


03-07-18 16:47:57 CEST
-----------------
2003-07-18 16:47:57 CEST
-----------------
18 lipiec 2003 16:47:57 CEST
-----------------
piątek, 18 lipiec 2003 16:47:57 CEST
-----------------


r



Metoda getXXXInstance() klasy DateFormat zwraca (zlokalizowany, jeśli można) obiekt klasy SimpleDateFormat. Za pomocą tej klasy możemy  zastosować wzorce formatowania do pokazywania (i parsowania) dat i czasu.

Wzorzec formatowania składa się z liter ('a' - 'z', 'A' - 'Z')), które mają specjalne znaczenie i są interpretowane jako składowe daty/czasu (lub zarezerwowane) oraz innych symboli, które są po prostu kopiowane przy formatowaniu. Litery ujęte w apostrofy nie są interpretowane.
Litery, mające specjalne znaczenie pokazuje tablica.

Litera  Znaczenie Typ Przykład
GEra  TekstAD
yRok Rok1996; 96
MMiesiąc w roku MiesiącJuly; Jul; 07
wTydzień w roku Liczba
27
WTydzień w  miesiącuLiczba2
DDzień roku Liczba189
dDzień miesiąca Liczba10
FDzień tygodnia  Liczba2
EDzień tygodnia TekstTuesday; Tue
aTekst  Am/pm  TekstPM
HGodzina dnia (0-23) Liczba0
kGodzina dnia (1-24) Liczba24
KGodzina  am/pm (0-11) Liczba0
hGdodzina am/pm (1-12)Liczba
12
mMinuta Liczba30
sSekunda Liczba55
SMilisekundaLiczba978
zStrefa czasowa Symbol strefy
Pacific Standard Time; PST; GMT-08:00
ZStrefa czasowa Symbol RFC 822 -0800

Poszczególne specjalne litery są we wzorcach formatowania powtarzane określoną liczbę razy.
Przy tym:
Przyjrzyjmy się  kilku przykładom zastosowania wzorców formatowania dat.

Poniższy program:
import java.util.*;

public class Daty1 {

  public static void main(String[] args) {

    Calendar c = Calendar.getInstance();
    Date teraz = c.getTime();

    SimpleDateFormat df = (SimpleDateFormat) DateFormat.getDateInstance();

    String[] pattern = {"dd-MM-yyyy",
                        "MMMM, 'dzień 'dd ( EE ), 'roku 'yyyy GGGG",
                        "EEEE, dd MMM yyyy 'r.'"
                       };
    for (int i=0; i<pattern.length; i++) {
      df.applyPattern(pattern[i]);
      System.out.println(df.format(teraz));
    }

  }

}
wyprowadzi:

18-07-2003
lipiec, dzień 18 ( Pt ), roku 2003 n.e.
piątek, 18 lip 2003 r.


Przy parsowaniu z użyciem zdefiniowanych wzorców formatowania teksty (zapisane zgodnie z tymi wzorcami) przekształcane są na daty (obiekty klasy Date). Reguły parsowania są podobne jak w przypadku klasy NaumberFormat (przypopmnijmy sobie omówione wczesniej zasady zgłaszania wyjątków oraz posługiwanie się klasą ParseException).
Poniższy przykładowy fragment:
   public static void main(String[] args) {


    SimpleDateFormat df = (SimpleDateFormat) DateFormat.getDateInstance();

    String[] pattern = {"dd-MM-yyyy",
                        "MMMM, 'dzień 'dd ( EE ), 'roku 'yyyy GGGG",
                        "EEEE, dd MMM yyyy 'r.'"
                       };

    for (int i=0; i<pattern.length; i++) {

      String in=JOptionPane.
                showInputDialog("Wprowadź datę wg wzorca " + pattern[i]);
      df.applyPattern(pattern[i]);
      Date data = df.parse(in, new ParsePosition(0));
      System.out.println(data);
    }
  }

po wprowadzeniu w dialogach tekstów:
12-12-1999
lipiec, dzień 18 ( Pt ), roku 2003 n.e.
wtorek, 12 lipiec 2003 r.

wyprowadzi na konsolę:
Sun Dec 12 00:00:00 CET 1999
Fri Jul 18 00:00:00 CEST 2003
Sat Jul 12 00:00:00 CEST 2003

Zwróćmy uwagę: błędny dzień tygodnia (wtorek zamiast soboty) nie spowodował błędu interpretacji, ale uzyskana data jest właściwa (nazwa dnia tygodnia została skorygowana).

Oczywiście, formatowanie i parsowanie podlega zasadom lokalizacji.
Istotnych informacji lokalizacyjnych dostarcza klasa DateFormatSymbols. Mieliśmy okazję z niej korzystać przy okazji uzyskiwania nazw dni tygodnia po polsku. Zobaczmy teraz jakich jeszcze informacji możemy się po niej spodziewać.

Przykładowy program tworzy obiekty klasy DateFormatSymbols dla kilku lokalizacji i wywołuje na ich rzecz metody takie jak getWeekdays() (zwracającą nazwę dni tygodnia) czy getMonths() (nazwy dni miesiąca). Nazwy metod pozyskujących zlokalizowane informacje są samoobjaśniające sie, wynik działania programu pokazujemy w obszarze wielowierszowego pola edycyhnego (JTextArea) po to by właściwie były interpretowane znaki Unicode (zob. rysunek).

import java.util.*;
import java.text.*;
import java.awt.*;
import javax.swing.*;

public class DateFormatSymbolsShow {

  String[] lang = { "fr", "es", "de", "ru" };

  String out = "";

  public DateFormatSymbolsShow() {
    for (int i=0; i<lang.length; i++) {
      Locale  loc = new Locale(lang[i]);
      DateFormatSymbols dfs = new DateFormatSymbols(loc);
      out += '\n' + loc.getDisplayLanguage();
      // nazwy er
      addToOut("Ery: ", dfs.getEras());
      // nazwy miesięcy
      addToOut("Miesiące: ", dfs.getMonths());
      // skróty miesięcy
      addToOut("Miesiące - skróty: ", dfs.getShortMonths());
      // nazwy dni tygodnia
      addToOut("Dni tygodnia: ", dfs.getWeekdays());
      // skróty nazw dni tygodnia
      addToOut("Dni tygodnia - skróty: ", dfs.getShortWeekdays());
    }
    JTextArea ta = new JTextArea(out);
    ta.setFont(new Font("Dialog", Font.BOLD, 14));
    JFrame f = new JFrame();
    f.getContentPane().add(ta);
    f.pack();
    f.show();
  }

  void addToOut(String msg, String[] s) {
    out += "\n" + msg;
    for (int i=0; i<s.length; i++) {
      out += ' ' + s[i];
    }
  }

  public static void main(String[] args) {
    new DateFormatSymbolsShow();
  }

}


r


Zwykle nie korzystamy z klasy DateFormatSymbols (jest ona używana automatycznei przy formatowaniu dat), Czasem jednak może zajść taka potrzeba. Wtedy można użyc konstruktora klasy SimpleDateFormat, dostarczając mu oprócz pierwszego argumentu (wzorca formatowania) argument drugi - refrencję do obiektu DateFormatSymbols.

Wykorzystamy to teraz do poprawieniu błędów gramatycznych, które nieuchronnie powstają przy formatowaniu dat w języku polskim ze względu na brak uwzględnienia właściwej odmiany nazw miesięcy (nb uważny Czytelnik zapewne zauważył, że lokalizacje dokonywane przez "lokalnych" ekspertów firm Sun i IBM są często nieco powierzchowne; błędy gramatyczne i ortograficzne dotyczą nie tylko polskiego, co można było spostrzec w kilku przykładach z tego rozdziału). Przy okazji zobaczymy, że właściwości lokalizacyjne formatowania dat można łatwo zmieniać (za pomocą rozlicznych metod set... z klasy DateFormatSymbols).

import java.util.*;
import java.text.*;

public class DateFormatPol {

  public static String polskaData(Date data) {
    String[] mies = { "stycznia", "lutego", "marca", "kwietnia",
                      "maja", "czerwca", "lipca", "sierpnia",
                      "września", "października", "listopada",
                      "grudnia"
                    };
    DateFormatSymbols dfs = new DateFormatSymbols();
    dfs.setMonths(mies);
    SimpleDateFormat df = new SimpleDateFormat("dd MMMM yyyy", dfs);
    return df.format(data);
  }



  public static void main(String[] args) {
    System.out.println( polskaData( new Date() ) );
  }

}

Prrzykładowy listing programui:
20 lipca 2003


1.7. Zlokalizowany rozbiór tekstów

W części A omawialiśmy rozbiór tekstów za pomocą klasy StringTokenizer. Oczywiście, zastosowane tam proste reguły wyodrębniania fragmentów napisów tak naprawdę nie nadają się do przetwarzania "języka pisanego".
Tę - może nie na codzień potrzebną, ale wartą odnotowania - funkcję spełnia klasa BreakIterator z pakietu java.text.  Warto też wspomnieć o tej klasie dlatego, że zastosowano w niej - na pierwszy rzut oka - mało intuicyjne, ale, okazuje się, wygodny mechanizm analizy składniowej.

Klasa ta pozwala dzielić tekst pisany w danym języku na elementy takie jak: zdania, wiersze (wiersz jest częścią zdania od początku linii do jej końca lub znaku przeniesienia), słowa, znaki.
Do wyodrębniania każdego z w/w elementów tekstu trzeba stworzyć odrębny iterator. Służą temy statyczne metody klasy: getSentenceInstance(...), getLineInstance(...), getWordInstance(...) , getCharacterInstance(...).

Metody iteratora (związanego z danym elementem tekstu) umożliwiają m.in.
Metody te nie tylko zwracają pozycję, ale przesuwają bieżącą pozycję podziału tekstu. Dlatego właśnie klasa została nazwana BreakIterator - jej metody pozwalają iterować poprzez pozycje podziału tekstu.

Ale uwaga! Iterator służący do wyodrębniania słów - w przeciwieństwie do innych iteratorów (np. zdań) - zwraca indeksy elementów (słów) oraz indeksy separatorów elementów-słów. To czy na danej pozycji zaczyna się akurat kolejne słowo czy też ciąg znaków dzielących słowa musimy sprawdzić samodzielnie.

Program testujący wraz z komentarzami pokazuje sposób użycia BreakIteratora.

import java.text.BreakIterator;

public class BreakIteratorTest {

  // Metoda wypisująca na konsoli
  //  - zdania - jeśli przekazano jako argument typ wartość 0
  //  - lub słowa - jeśli przekazano jako argument typ wartość 1
  // wyodrębnione z tekstu txt

  private static void show(int typ, String txt) {
    String[] head = { "Zdania", "Słowa" }; // Nagłówek: dla typ==0 "Zdania"
                                           //           dla typ==1 "Słowa"

    // Uzyskanie odpowiedniego breakiteratora
    // dla typ == 0 będzie to iterator "po zdaniach"
    // dla typ == 1 będzie to iterator "po słowach"
    BreakIterator bri = (typ == 0 ? BreakIterator.getSentenceInstance()
                                  : BreakIterator.getWordInstance()
                        );
    System.out.println(head[typ]); // Wypisanie nagłówka

    // Po stworzeniu iteratora a przed jego użyciem
    // należy ustalić tekst, który będzie przetwarzany

    bri.setText(txt);

    // Iterowanie

    int start = bri.first();  // pozycja pierwszego podziału na elementy
    int end   = bri.next();   // pozycja następnego podziału na elementy

    while (end != BreakIterator.DONE) { // dopóki można dzielić tekst

      // Wyodrębniamy element
      String elt = txt.substring( start, end);

      // Wypisujemy element jeżeli to jest iterator dla zdań
      // albo jeżeli to jest iterator dla słów, a elementem jest słowo
      // Czy to jest słowo stwierdzamy za pomocą metody
      // isIteratorWord(...) - zob. dalej

      if (typ == 0 | (typ == 1 && isIteratorWord(elt)))
         System.out.println(start + ": " +  elt);


      start = end;        // początek następnego (po wypisanym) elementu
      end = bri.next();   // pozycja  następnego podziału
    }
  }

  // Metoda stwierdza czy element wyodrębniony przez iterator słów
  // jest słowem. Będzie nim każdy element, który zawiera choć jedną
  // literę lub cyfrę. Czy dany znak jest literą lub cyfrą
  // stwierdzamy za pomocą statycznej metody z klasy Character
  // Character.isLetterOrDigit(znak)

  private static boolean isIteratorWord(String elt) {
    int l = elt.length();
    for (int i=0; i < l; i++)
      if (Character.isLetterOrDigit(elt.charAt(i))) return true;
    return false;
  }


  public static void main(String[] args) {

    String s = "Ala Kot-Kotowska ma kota. A mleko? "+
               "Pies go wychłeptał (swobodnie) - 0.1 litra. " +
               "Tak? Nie! Chyba nie... A może? Byłażby to \"hańba\"?!!!";

    show(0, s); // pokaż zdania
    show(1, s); // pokaż słowa
  }

}
Wydruk działania programu pokazano .... poniżej:

Zdania
0: Ala Kot-Kotowska ma kota.
26: A mleko?
35: Pies go wychłeptał (swobodnie) - 0.1 litra.
79: Tak?
84: Nie!
89: Chyba nie...
102: A może?
110: Byłażby to "hańba"?!!!
Słowa
0: Ala
4: Kot-Kotowska
17: ma
20: kota
26: A
28: mleko
35: Pies
40: go
43: wychłeptał
55: swobodnie
68: 0.1
72: litra
79: Tak
84: Nie
89: Chyba
95: nie
102: A
104: może
110: Byłażby
118: to
122: hańba


BreakIterator pozwala dość wygodnie działać na tekstach. Oczywiście - i to jest jego równie ważna zaleta - BreakIterator przygotowany jest do przetwarzania tekstów zgodnie z ustawieniami lokalizacyjnymi (dla różnych języków). Inaczej wygląda tekst i jego podział na zdania czy słowa w języku niemieckim, inaczej w angielskim, inaczej w chińskim. BreakIterator będzie umiał dokonać właściwego rozbioru w każdym z tych przypadkóe.
Tak jak zawsze w przypadku klas czułych na lokalizację ustawić odpowiednie przetwarzanie można za pomocą metod statycznych. które zwracają iterator dla odpowiednich elementów (zdania, słowa, itp.), a mają za argument referencję do obiektu klasy Locale, który określa lokalizację.

1.8. Porównywanie i sortowanie napisów

Różne języki implikują różny alfabetyczny porządek napisów.
Właściwe porównywanie napisów możemy przeprowadzić za pomocą obiektu klasy Collator z pakietu java.text.

Jest to klasa czuła na lokalizację, zatem właściwą instancję kolatora dla domyślnej lokalizacji uzyskamy za pomocą odwołania Collator.getInstance(). Możemy też uzyskać kolator dla dowolnej lokalizacji, podając w metodzie getInstance(...) argument-lokalizację

Mając obiekt-kolator dla okreslonej (domyślnej lub podanej) lokalizacji możemy za pomocą metody compare(...) wywołanej na jego rzecz uzyskać własciwy dla danej lokalizacji wynik porównania dwóch napisów podanych jako argumenty metody.

Łatwo się domyślić, że klasa Collator implementuje interfejs Comparator. Zatem porównania uzyskiwane za pomoca metody compare(...) kolatora łatwo uczynić podstawą sortowania tablic i kolekcji, a także decydowania o dodawaniu elementów do uporządkowanych zbiorów i map.

Zobaczmy najprostszy przykład. Niech domyślna lokalizacja będzie lokalizacją polską, Jak posortować tablicę napisów w tej lokalizacji ? Reguła jest prosta:
Poniższy program - oprócz wspomnianej wyżej procedury - pokazuje, że porządek napisów w róznych lokalizacjach jest różny (przy sortowaniu polskich napisów powinniśmy oczywiście stosować kolator dla lokalizacji polskiej)\.

import java.util.*;
import java.text.*;

public class Kolator0 {

  static void sortShow(String msg, String[] txt, Collator col) {
    String[] copyTxt = (String[]) txt.clone();
    Arrays.sort(copyTxt, col);
    System.out.println(msg);
    for (int i=0; i < copyTxt.length; i++) {
      System.out.println(copyTxt[i]);
    }
  }


  public static void main(String[] args) {
    String[] txt = { "bela", "Ala", "ą", "Ą", "ą", "ala" , "Be", "Ala",
                     "alabama", "be", "Be", "1", "ć", "my", "My", "Myk", "myk"  };
    Collator col = Collator.getInstance();
    sortShow("Sort pol", txt, col);
    Collator col1 = Collator.getInstance(new Locale("en"));
    sortShow("Sort en", txt, col1);
  }

}
Program wyprowadzi na konsolę:

Sort pol
1
ala
Ala
Ala
alabama
ą
ą
Ą
be
Be
Be
bela
ć
my
My
myk
Myk
Sort en
1
ą
ą
Ą
ala
Ala
Ala
alabama
be
Be
Be
bela
ć
my
My
myk
Myk


W jaki sposób (dla różnych lokalizacji) uzyskujemy właściwe porównania?
Otóż konkretne kolatory są obiektami klasy RuleBasedCollator, która jest podklasą klasy Collator.
Uporządkowanie za pomocą kolatorów klasy RuleBasedCollator odbywa się na podstawie porównywanie znaków w oparciu o reguły zapisane za pomocą prostej składni. Reguły te określają cztery (a w pakieice ICU nawet pięć) porządki:
Stosowany dla danego kolatora porządek nazywa się siłą kolatora.
Nawet nie znając reguł danego kolatora możemy ustalać jego siłę (np. czy napisy różniące się tylko wielkością liter mają być rozrózniane). Służy temu metoda setStrength(...) z argumentem określającym siłę kolatora (jedna ze stałych statycznych  klasy Collator o nazwach PRIMARY, SECONDARY, TERTIARY, IDENTICAL).

W modyfikacji poprzedniego przykładowego programu ustalimy siłę kolatora na PRIMARY (domyślnie jest TERTIARY). W polskiej lokalizacji (wedle reguł dla niej okreslonych) nie będą teraz rozróżniane wielkie i małe litery.

  public static void main(String[] args) {
    String[] txt = { "bela", "Ala", "ą", "Ą", "ą", "ala" , "Be", "Ala",
                     "alabama", "be", "Be", "1", "ć", "my", "My", "Myk", "myk"  };
    Collator col = Collator.getInstance();
    sortShow("Sort TERTIARY", txt, col);
    col.setStrength(Collator.PRIMARY);
    sortShow("Sort PRIMARY", txt, col);
}
Program wyprowadzi napisy w dwóch różnych porządkach, odpowiadjących dwóm różnym siłom kolatora:

Sort TERTIARY
1
ala
Ala
Ala
alabama
ą
ą
Ą
be
Be
Be
bela
ć
my
My
myk
Myk
Sort PRIMARY
1
Ala
ala
Ala
alabama
ą
Ą
ą
Be
be
Be
bela
ć
my
My
Myk
myk


Oczywiście, możemy sami definiować reguły dla obiektów klasy RuleBasedCollator.
Reguły są zapisywane jak łańcuchy znakowe w postaci:

    <relacja> tekst <relacja> tekst ....

gdzie relacja wprowadzana jest za pomocą znaków:
<  - większe wedle pierwszego porządku (rozróżniania liter)
;  -  większe wedle drugirgo porządku (rozróżniania akcentowanych liter),
,   - większe wedle trzeciego porządku (wielkość liter)
= - równe
a tekst jest dowolym ciągiem znaków wyłączając znaki specjalne i znaki opisujące w/w relacje (jeśli takie znaki chcemy włączyć do porównań ujmujemy je w apostrofy).

Na przykład:

9 < a, A < b, B < c, C

Możemy też użyć znaku &, który łogicznie łączy  reguły np:
a < b & b < c
jest identyczne z :
a < b < c

przy tym jednak należy uważać na kolejność, bowiem połączone reguły są stosowane w kolejności od lewej do prawej i np.
a < c & a < b
nie jest tożsame z
a < b & a < c

Stworzenie pełnego zestawu reguł dla kolatora może być dość pracochłonne. trzeba bowiem uwzględnić wiele możliwych znaków.
Zobaczmy najpierw jak wygląda fragment reguł dla kolatora w lokalizacji polskiej.
Reguły te możemy uzyskać za pomocą odwołania:
    Collator col = Collator.getInstance();
    String rules = ((RuleBasedCollator) col).getRules();
i pokazać np. w oknie:

r


Spróbujmy teraz rozpatrzyć uproszczony przykład (abstrahując od wielu możliwych znaków). Przykłady sortowania pokazały nam, że  rozróżnienie pomiędzy dużymi i małymi literami  nie jest pierwszorzędne: tylko w przypadku gdy napisy są takie same kolator bierze pod uwagę tę różnicę i ustawia wtedy małe litery przed wielkimi. Widać to zresztą w zestawie reguł np. ... < a, A < b, B  rozróznia najpierw litery a i b (bez uwzględneinia ich wielkości), a dopiero gdy napisy (składający się z tych liter) są takie same bierze pod uwagę
 ich wielkość.
 Stworzymy więc przykładowy inny zestaw reguł, który (przy sortowaniu rosnącym) ustawi wyraz  "Polska" na początku, a inne napisy posortuje w porządku - najpierw duże litery, później małe (z uwzględnieniem polskich znaków i tego, że polskie odpsoiedniki znaleźc sie mają po "normalnych" literach np. ą po a).
Obrazuje to poniższy program:
import java.util.*;
import java.text.*;

public class Kolator1 {

  static void sortShow(String msg, String[] txt, final Collator col) {
    String[] copyTxt = (String[]) txt.clone();
    Arrays.sort(copyTxt, col);
    System.out.println(msg);
    for (int i=0; i < copyTxt.length; i++) {
      System.out.println(copyTxt[i]);
    }
  }


  public static void main(String[] args) {

    // Napisy do posortowania
    String[] txt = { "bela", "Ala", "ą", "Ą", "ą", "ala" , "Be", "Ala",
                     "alabama", "be", "Be", "1", "Ćwikła", "ćwikła",
                     "ćwikla", "Polska",
                     "My", "my", "Myk", "myk"  };

    // Domyślny polski kolator
    Collator col = Collator.getInstance();
    sortShow("Default sort", txt, col);

    // Nowe reguły
    String rules = " < Polska < A < Ą < B < C < Ć < D < E < Ę < F < G < H" +
                  " < I < J < K < L < Ł < M < N < Ń < O < P < Q < R < S < Ś" +
                  " < T < U < V < W < X < Y < Z < Ź " +
                  " < a < ą < b < c < ć < d < e < ę < f < g < h" +
                  " < i < j < k < l < ł < m < n < ń < o < p < q < r < s < ś" +
                  " < t < u < v < w < x < y < z < ź";

    try {
      col = new RuleBasedCollator(rules);
    } catch (ParseException exc) {
        System.out.println("Wadliwa reguła na pozycji " + exc.getErrorOffset());
        System.out.println(exc);
        System.exit(1);
    }

    sortShow("My new rules sort", txt, col);

  }

}
który wyprowadzi:

Default sort
1
ala
Ala
Ala
alabama
ą
ą
Ą
be
Be
Be
bela
ćwikla
ćwikła
Ćwikła
my
My
myk
Myk
Polska

My new rules sort
Polska
Ala
Ala
Ą
Be
Be
Ćwikła
My
Myk
ala
alabama
ą
ą
be
bela
ćwikla
ćwikła
my
myk
1


Jeżeli sortowanie jakiegoś zestawu napisów ma się powtarzać wielokrotnie, to dla zwiększenia efektywności działania można przyporządkować napisom klucze i sortować te klucze (klucze są sortowane szybciej).
Sposób postępowania jest następujący:
Ilustruje to poniższy program.
import java.util.*;
import java.text.*;

public class Kolator2 {

  public static void main(String[] args) {

    // Napisy do posortowania
    String[] txt = { "bela", "Ala", "ą", "Ą", "ą", "ala" , "Be", "Ala",
                     "alabama", "be", "Be", "1", "Ćwikła", "ćwikła",
                     "ćwikla", "Polska",
                     "My", "my", "Myk", "myk"  };

    // Domyślny polski kolator
    Collator col = Collator.getInstance();

    // Lista kluczy
    List keys = new ArrayList();

    // Uzyskanie kluczy dla napisów
    // wartości kluczy uzyskujemy od kolatora
    for (int i=0; i<txt.length; i++) {
      CollationKey key = col.getCollationKey( txt[i] );
      keys.add(key);
    }

    // Sortowanie
    // porównywane mogą być tylko klucze uzyskane od tego samego kolatora!

    Collections.sort(keys);

    // Pokazanie wyniku
    // mamy klucze ułożone w określonym porządku napisów, które reprezentują
    // musimy pobrać napis skojarzony z kluczem

    for (Iterator it = keys.iterator(); it.hasNext(); ) {
      CollationKey key = (CollationKey) it.next();
      String napis = key.getSourceString();
      System.out.println(napis);
    }
  }

}


1.9. Internacjonalizacja aplikacji i dodatkowe zasoby (resource bundle)

Nie tylka liczby, daty, czas powinny być w aplikacjach przygotowane do prezentacji zgodnie z wymaganiami  danej lokalizacji. Również komunikacja aplikacji z użytkownikiem powinna przebiegać w języku  użytkownika.
Wszelkiego rodzaju napisy i komunikaty dla użytkownika powinny być lokalizacyjnie przygotowane.
Częstym błędem przy tworzeniu aplikacji "produkcyjnych" jest nieuwzględnianie tego warunku. Oto ktoś przygotowuje aplikację, używając do komunikowania się z użytkownikiem napisów w jezyku polskim. Wszystko działa poprawnie, klient jest zadowolony. Nagle okazuje się, że ta sama aplikacja powinna działać w londyńskiej centrali firmy i wszystkie komunikaty, napisy itp. trzeba przerabiać na angielski. Kosztuje to wiele wysiłku, powoduje powstawanie błędów w programie itd.

Generalnie więc aplikacja powinna być od początku przygotowana na działanie w różnych środowiskach językowych. A to oznacza konieczność odseparowania językowych właściwości apliakcji (takich jak język komunikatów) od samego jej kodu.

Mechanizmem umożliwiającycm takie odseparowanie w Javie są tzw. dodatkowe zasobu (ResourceBundle).
Istnieją dwa rodzaje dodatkowych zasobów: oparte na klasach (ListResourceBoundle) i na czystych, tekstowych plikach właściwości (PropertiesResourceBundle).

Pliki właściwości umożliwiają odseparowanie napisów, klasy ListResourceBundle - dowolnych obiektów.

Aby odseparować napisy i dostarczyć ich różnych wersji jezykowych za pomocą plików właściwości:
tworzymy plik właściwości o wybranej nazwie i rozszerzeniu np. HelloMessages.properties
w pliku tym zapisujmey pary: klucze = napis,
dla każdej lokalizacji branej pod uwagę w naszej aplikacji towrzymy dodatkowy plik właściwości o nazwie uzupelnionej o sufiks, wskazujący na lokalizacje (np. HelloMessages_pl.properties)
zapisujemy w nim (pod tymi samymi kluczami) napisy w języku danej lokalizacji.
w naszym programie używamy zdefiniowanych napisów poprzez mechanizm odwołania do do określonego przez aktualną lokalizację dodatkowego zasobu (np. ResourceBundle msgs = ResourceBundle.getBundle("HelloMessages", lokalizacja) i pobranie napisu, znajdującego się pod podanym kluczen (np. String value = msgs.getString(key);
Java odnajdzie odpowiedni plik właściwości (np. HelloMessages_pl.properties) i pobierze z niego napis, odpowidające podanemu kluczowi; jeśli takiego pliku nie będzie - to zostanie wybrany plik o możliwie bliskiej nazwie (np. gdy szukany jest  niestniejący plik HelloMessages_pl_PL.properties, to wynikiem może być istniejący plik HelloMessages_pl.properties) albo plik bez sufiksu w nazwie, który pełni rolę domyślnego przy braku odpowiednio zlokaalizowanych.

Jako prosty przykład stworzymy dwa pliki wartości klucz=napis: domyślny i dla lokalizacji polskiej:

Plik HelloMessages.properties
# Komunikaty w aplikacji Hello - domyślne przy braku pliku dla danej lokalizacji
hello = Hello!
bye = Good bye!

Plik HelloMessages_pl.properties
# Komunikaty w aplikacji Hello - po polsku
hello = Dzień dobry!
bye = Do widzenia.

Uwaga: klucze (hello, bye) są takie same.

Plików tych i mechanizmu dodatkowych zasobów użyjemy w prościutkiej aplikacji Hello (dzięki czemu komunikacja z użytkownikiem naprawdę będzie odseparowana od kodu źródłowego).
import java.util.*;

public class Hello {

  static void sayHello() {
    Locale defLoc = Locale.getDefault();
    ResourceBundle msgs =
                   ResourceBundle.getBundle("HelloMessages", defLoc);
    String powitaj  = msgs.getString("hello");
    String pozegnaj = msgs.getString("bye");
    System.out.println(powitaj);
    System.out.println(pozegnaj);
 }


  public static void main(String[] args) {
    sayHello(); // tutaj działa domyślna lokalizacja pl_PL
    // zmieniamy domyślną lokalizację
    Locale.setDefault(new Locale("en"));
    sayHello();
  }

}
Wynik działania programu:

Dzień dobry!
Do widzenia.
Hello!
Good bye!

Uwagi:
Tak naprawdę ResourceBundle.getBundle(..) szuka najpierw klas dziedziczących ListResourceBundle, a dopiero później plików właściwości.

Użycie ListResourceBundle wymaga od nas pisania kodu (i to jest wada w stosunku do mechanizmu plików właściwości), ale równocześnie pozwala na wprowadzenie "pod kluczami" - innych obiektów niż napisy (np. jakichś liczb, obrazów, dźwięków - oczywiścię zlokalizowancyh).

Nasze zlokalizowane listy zasobów są klasami, które:
Przykład:
public class CountryInfo_pl_PL extends ListResourceBundle {

   public Object[][] getContents() {
	return contents;
    }

    private Object[][] contents = {
	{ "name", "Polska" },
	{ "flag",  new ImageIcon("PolskaFlaga.gif" },
    };
}

Uzyskiwanie wartości z takich zasobó odbywa się w następujący sposób:

ResourceBundle info =
		ResourceBundle.getBundle("CountryInfo", currentLocale);

String nazwaKraju = info.getString("name");
ImageIcon flaga = (ImageIcon) info.getObject("flag");
Uwaga: jeżeli wartość nie jest Stringiem należy zastosować metodę getObject() i dokonać zawężającej konwersji do właściwego typu.


Dodatkowe zasoby (w specjalnej formie) razem z formatorem MessageFormat są używane do generowania złożonych komunikatów, które opisywane są za pomocą szablonu z wymiennymi parametrami.

Opanowanie tego zagadnienia pozstawiamy Czytelnikowi jako dodatkowe ćwiczenie z dokumentacją (klasa MessageFormat)



1.10. Zadania i ćwiczenia

  1. Dokonać internacjonalizacji aplikacji Owoce (z części B), tak by możliwe było jej własciwe działanie w lokalizacji polskiej i angielskiej. Przy wydruku rachunku  z kasy podać godzinę. Uwzględnić właściwie dla danych lokalizacji: nazwy owoców, formaty liczb, waluty, formaty dat i czasu.
  2. Samodzielnie opanować posługiwanie się klasą MessageFormat. Użyć jej dla zlokalizowanego traktowania liczby mnogiej liczebników (np. 1 kosz owowców, 2 kosze owoców). W jaki sposób możemy tu użyć klasy ChoiceFormat? A jak zdefiniuejmy reguły dla korzystania z klasy RuledBasedFormat pakietu ICU?