Formatowanie, lokalizacja i internacjonalizacja


Pierwsze spotkanie z lokalizacją


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

wynik:

3.3333333333333335

Sformatować wynik.

     NumberFormat nf = NumberFormat.getInstance();
nf.setMaximumFractionDigits(3);
String wynik = nf.format(d);
System.out.println(wynik);

wynik (na komputerze w Polsce):

3,333


wynik (na komputerze w Anglii):

3.333

Co się dzieje? Dlaczego?

Lokalizacje

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ą
.

Potrzebny jest mechanizm lokalizacji i internacjonalizacji aplikacji, co wynika z kosztów dostosowania (zmian kodu).

W Javie cały zestaw klas:
java.text i trochę w java.util.

Taligent ====> Sun zastosował w Javie 1.1. 
Do dziś bez większych zmian.

Taligent ====> Unicode group w IBM Globalization Center of Competency w Cupertino (rozwój, przenoszenie do języków C++i C, na zasadach "open source").

Bblioteka  International Components for Unicode:
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://www.icu-project.org/

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 - kombinacja dwóch małych liter, standard ISO-639 (zob dostępne kody : http://www.ics.uci.edu/pub/ietf/http/related/iso639.txt )

Kod kraju - to  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 - dodatkowa  informacja, 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");

Jakie są dostępne lokalizacje?

Locale[] loc = Locale.getAvailableLocales();
Domyślna lokalizacja - na podstawie właściwości ustalonych dla platformy systemowej.

Można się dowiedzieć:
Locale dloc = Locale.getDefault();
i zmienić:
Locale newDefLoc = new Locale(...);
Locale.setDefault(newDefLoc)


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
ScannerSkanowanie tekstów z różnych źródeł
FormatterFormatowanie danych


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


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ę:

     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: w/w getInstance() nie dotyczą formtora i skanera.


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











Formatowanie liczb

NumberFormat.
Ale 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.


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);
Wynik: 3,333.

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
& lt; /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 ]


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 (np. polska z przecinkiem).

Jak uzyskać kropkę zamiast przecinka?
    DecimalFormat df = (DecimalFormat)
NumberFormat.getInstance(new Locale("en", "US"));
df.applyPattern(format);

Problem z walutą (będzie USD).
Można: setCurrency(...).
Albo nie zmieniać lokalizacji, a zmienić symbole formatora decymalnego:

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
Inne symbole używane przy formatowaniu i metody ich zmian opisane są w dokumentacji.

Specjalne formatory do formatowania:
Prostsza drogą uzyskiwania efektów podobnych do użycia wzorców formatowania w szczególnych przypadkach.

Formatory potrafią także dokonywać przekształceń odwrotnych: zamieniać napisy reprezentujące liczby na postać binarną tych liczb.
Np. tekst, w którym liczby podawane są z przecinkami jako separatorami miejsc dziesiętnych, Metoda parseDouble z klasy Double nie da oczekiwanych wyników (wyjątek).

Należy 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).

Metoda parse(String) użyta na rzecz formatora.
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ł



Przy okazji: spójrzmy na nowe środki Javy 1.5

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

public class Parse2j5 {

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)
// GENERIC!!! List<Double> numList = new ArrayList<Double>(); 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.doubleValue()); // BOXING!!! 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( Double val : numList ) { // FOR-EACH System.out.println(val); suma += val; // UNBOXING!!! } /*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)); } }
Kontrola wyjścia - sposobu formatowania:  klasa 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.


Klasa ChoiceFormat jest szczególnie użyteczna przy internacjonalizacji napisów w programie z wykorzystaniem klasy MessageFormat.

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:

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.


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.

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.
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
--------------------------------------------------


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.


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.


Możemy  uzyskać inne kalendarze (niż domyślny):
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?
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


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.

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 .
Oprócz tego możemy posłużyć się wzorcami formatowania.

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

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.
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.

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. 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


Zlokalizowany rozbiór tekstów

zob. w książce

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:

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).


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

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);
}
}

}


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.

Generalnie 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.

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ów 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.

Np.

Number val;
Object[] elts = { new Date(), val };

String format = "Dnia {0,date} o godzinie {0,time}" +
                       " wypłacono {1,number,currency}" },
   
MessageFormat.format(format, elts);


Przykład: Msg.java


Java 1.5 - metody ze zmienną liczbą argumentów:

  private void setBlue(Component ... comps) {
    for (Component c : comps) c.setForeground(Color.blue);
    println("Liczba przekazanych kompoenntów: " + comps.length);
    println("Szerokość pierwszego komponentu: " +
                        comps[0].getWidth());
    println("Szerokość ostatniego komponentu: " +
                        comps[comps.length-1]);
  }

i wywolanie:

setBlue(lab1, lab2, lab3);

setBlue(button1, button2);  // nie trzeba tablic !!!


Statyczna metoda format z klasy MessagFormat jest zdefiniowana jako metoda ze zmienną liczbą argiemnetów.

Przykład: Msg1.java