001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *  
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *  
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License. 
018 *  
019 */
020package org.apache.directory.shared.util;
021
022
023import java.text.DecimalFormat;
024import java.text.NumberFormat;
025import java.text.ParseException;
026import java.util.Calendar;
027import java.util.TimeZone;
028
029import org.apache.directory.shared.i18n.I18n;
030
031
032/**
033 * <p>This class represents the generalized time syntax as defined in 
034 * RFC 4517 section 3.3.13.</p>
035 * 
036 * <p>The date, time and time zone information is internally backed
037 * by an {@link java.util.Calendar} object</p>
038 * 
039 * <p>Leap seconds are not supported, as {@link java.util.Calendar}
040 * does not support leap seconds.</p>
041 * 
042 * <pre>
043 * 3.3.13.  Generalized Time
044 *
045 *  A value of the Generalized Time syntax is a character string
046 *  representing a date and time.  The LDAP-specific encoding of a value
047 *  of this syntax is a restriction of the format defined in [ISO8601],
048 *  and is described by the following ABNF:
049 *
050 *     GeneralizedTime = century year month day hour
051 *                          [ minute [ second / leap-second ] ]
052 *                          [ fraction ]
053 *                          g-time-zone
054 *
055 *     century = 2(%x30-39) ; "00" to "99"
056 *     year    = 2(%x30-39) ; "00" to "99"
057 *     month   =   ( %x30 %x31-39 ) ; "01" (January) to "09"
058 *               / ( %x31 %x30-32 ) ; "10" to "12"
059 *     day     =   ( %x30 %x31-39 )    ; "01" to "09"
060 *               / ( %x31-32 %x30-39 ) ; "10" to "29"
061 *               / ( %x33 %x30-31 )    ; "30" to "31"
062 *     hour    = ( %x30-31 %x30-39 ) / ( %x32 %x30-33 ) ; "00" to "23"
063 *     minute  = %x30-35 %x30-39                        ; "00" to "59"
064 *
065 *     second      = ( %x30-35 %x30-39 ) ; "00" to "59"
066 *     leap-second = ( %x36 %x30 )       ; "60"
067 *
068 *     fraction        = ( DOT / COMMA ) 1*(%x30-39)
069 *     g-time-zone     = %x5A  ; "Z"
070 *                       / g-differential
071 *     g-differential  = ( MINUS / PLUS ) hour [ minute ]
072 *     MINUS           = %x2D  ; minus sign ("-")
073 *
074 *  The <DOT>, <COMMA>, and <PLUS> rules are defined in [RFC4512].
075 *
076 *  The above ABNF allows character strings that do not represent valid
077 *  dates (in the Gregorian calendar) and/or valid times (e.g., February
078 *  31, 1994).  Such character strings SHOULD be considered invalid for
079 *  this syntax.
080 *
081 *  The time value represents coordinated universal time (equivalent to
082 *  Greenwich Mean Time) if the "Z" form of <g-time-zone> is used;
083 *  otherwise, the value represents a local time in the time zone
084 *  indicated by <g-differential>.  In the latter case, coordinated
085 *  universal time can be calculated by subtracting the differential from
086 *  the local time.  The "Z" form of <g-time-zone> SHOULD be used in
087 *  preference to <g-differential>.
088 *
089 *  If <minute> is omitted, then <fraction> represents a fraction of an
090 *  hour; otherwise, if <second> and <leap-second> are omitted, then
091 *  <fraction> represents a fraction of a minute; otherwise, <fraction>
092 *  represents a fraction of a second.
093 *
094 *     Examples:
095 *        199412161032Z
096 *        199412160532-0500
097 *
098 *  Both example values represent the same coordinated universal time:
099 *  10:32 AM, December 16, 1994.
100 *
101 *  The LDAP definition for the Generalized Time syntax is:
102 *
103 *     ( 1.3.6.1.4.1.1466.115.121.1.24 DESC 'Generalized Time' )
104 *
105 *  This syntax corresponds to the GeneralizedTime ASN.1 type from
106 *  [ASN.1], with the constraint that local time without a differential
107 *  SHALL NOT be used.
108 *
109 * </pre>
110 */
111public class GeneralizedTime implements Comparable<GeneralizedTime>
112{
113
114    /**
115     * The format of the generalized time.
116     */
117    public enum Format
118    {
119        /** Time format with minutes and seconds, excluding fraction. */
120        YEAR_MONTH_DAY_HOUR_MIN_SEC,
121        /** Time format with minutes and seconds, including fraction. */
122        YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION,
123
124        /** Time format with minutes, seconds are omitted, excluding fraction. */
125        YEAR_MONTH_DAY_HOUR_MIN,
126        /** Time format with minutes seconds are omitted, including fraction. */
127        YEAR_MONTH_DAY_HOUR_MIN_FRACTION,
128
129        /** Time format, minutes and seconds are omitted, excluding fraction. */
130        YEAR_MONTH_DAY_HOUR,
131        /** Time format, minutes and seconds are omitted, including fraction. */
132        YEAR_MONTH_DAY_HOUR_FRACTION
133    }
134
135    /**
136     * The fraction delimiter of the generalized time.
137     */
138    public enum FractionDelimiter
139    {
140        /** Use a dot as fraction delimiter. */
141        DOT,
142        /** Use a comma as fraction delimiter. */
143        COMMA
144    }
145
146    /**
147     * The time zone format of the generalized time.
148     */
149    public enum TimeZoneFormat
150    {
151        /** g-time-zone (Zulu) format. */
152        Z,
153        /** g-differential format, using hour only. */
154        DIFF_HOUR,
155        /** g-differential format, using hour and minute. */
156        DIFF_HOUR_MINUTE
157    }
158
159    private static final TimeZone GMT = TimeZone.getTimeZone( "GMT" );
160
161    /** The user provided value */
162    private String upGeneralizedTime;
163
164    /** The user provided format */
165    private Format upFormat;
166
167    /** The user provided time zone format */
168    private TimeZoneFormat upTimeZoneFormat;
169
170    /** The user provided fraction delimiter */
171    private FractionDelimiter upFractionDelimiter;
172
173    /** the user provided fraction length */
174    private int upFractionLength;
175
176    /** The calendar */
177    private Calendar calendar;
178
179
180    /**
181     * Creates a new instance of GeneralizedTime, based on the given Calendar object.
182     * Uses <pre>Format.YEAR_MONTH_DAY_HOUR_MIN_SEC</pre> as default format and
183     * <pre>TimeZoneFormat.Z</pre> as default time zone format. 
184     *
185     * @param calendar the calendar containing the date, time and timezone information
186     */
187    public GeneralizedTime( Calendar calendar )
188    {
189        if ( calendar == null )
190        {
191            throw new IllegalArgumentException( I18n.err( I18n.ERR_04358 ) );
192        }
193
194        this.calendar = calendar;
195        upGeneralizedTime = null;
196        upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION;
197        upTimeZoneFormat = TimeZoneFormat.Z;
198        upFractionDelimiter = FractionDelimiter.DOT;
199        upFractionLength = 3;
200    }
201
202
203    /**
204     * Creates a new instance of GeneralizedTime, based on the
205     * given generalized time string.
206     *
207     * @param generalizedTime the generalized time
208     * 
209     * @throws ParseException if the given generalized time can't be parsed.
210     */
211    public GeneralizedTime( String generalizedTime ) throws ParseException
212    {
213        if ( generalizedTime == null )
214        {
215            throw new ParseException( I18n.err( I18n.ERR_04359 ), 0 );
216        }
217
218        this.upGeneralizedTime = generalizedTime;
219
220        calendar = Calendar.getInstance();
221        calendar.setTimeInMillis( 0 );
222        calendar.setLenient( false );
223
224        parseYear();
225        parseMonth();
226        parseDay();
227        parseHour();
228
229        if ( upGeneralizedTime.length() < 11 )
230        {
231            throw new ParseException( I18n.err( I18n.ERR_04360 ), 10 );
232        }
233
234        // pos 10: 
235        // if digit => minute field
236        // if . or , => fraction of hour field
237        // if Z or + or - => timezone field
238        // else error
239        int pos = 10;
240        char c = upGeneralizedTime.charAt( pos );
241        if ( '0' <= c && c <= '9' )
242        {
243            parseMinute();
244
245            if ( upGeneralizedTime.length() < 13 )
246            {
247                throw new ParseException( I18n.err( I18n.ERR_04361 ), 12 );
248            }
249
250            // pos 12: 
251            // if digit => second field
252            // if . or , => fraction of minute field
253            // if Z or + or - => timezone field
254            // else error
255            pos = 12;
256            c = upGeneralizedTime.charAt( pos );
257            if ( '0' <= c && c <= '9' )
258            {
259                parseSecond();
260
261                if ( upGeneralizedTime.length() < 15 )
262                {
263                    throw new ParseException( I18n.err( I18n.ERR_04362 ), 14 );
264                }
265
266                // pos 14: 
267                // if . or , => fraction of second field
268                // if Z or + or - => timezone field
269                // else error
270                pos = 14;
271                c = upGeneralizedTime.charAt( pos );
272                if ( c == '.' || c == ',' )
273                {
274                    // read fraction of second
275                    parseFractionOfSecond();
276                    pos += 1 + upFractionLength;
277
278                    parseTimezone( pos );
279                    upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION;
280                }
281                else if ( c == 'Z' || c == '+' || c == '-' )
282                {
283                    // read timezone
284                    parseTimezone( pos );
285                    upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC;
286                }
287                else
288                {
289                    throw new ParseException( I18n.err( I18n.ERR_04363 ), 14 );
290                }
291            }
292            else if ( c == '.' || c == ',' )
293            {
294                // read fraction of minute
295                parseFractionOfMinute();
296                pos += 1 + upFractionLength;
297
298                parseTimezone( pos );
299                upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_FRACTION;
300            }
301            else if ( c == 'Z' || c == '+' || c == '-' )
302            {
303                // read timezone
304                parseTimezone( pos );
305                upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN;
306            }
307            else
308            {
309                throw new ParseException( I18n.err( I18n.ERR_04364 ), 12 );
310            }
311        }
312        else if ( c == '.' || c == ',' )
313        {
314            // read fraction of hour
315            parseFractionOfHour();
316            pos += 1 + upFractionLength;
317
318            parseTimezone( pos );
319            upFormat = Format.YEAR_MONTH_DAY_HOUR_FRACTION;
320        }
321        else if ( c == 'Z' || c == '+' || c == '-' )
322        {
323            // read timezone
324            parseTimezone( pos );
325            upFormat = Format.YEAR_MONTH_DAY_HOUR;
326        }
327        else
328        {
329            throw new ParseException( I18n.err( I18n.ERR_04365 ), 10 );
330        }
331
332        // this calculates and verifies the calendar
333        try
334        {
335            calendar.getTimeInMillis();
336        }
337        catch ( IllegalArgumentException iae )
338        {
339            throw new ParseException( I18n.err( I18n.ERR_04366 ), 0 );
340        }
341
342        calendar.setLenient( true );
343    }
344
345
346    private void parseTimezone( int pos ) throws ParseException
347    {
348        if ( upGeneralizedTime.length() < pos + 1 )
349        {
350            throw new ParseException( I18n.err( I18n.ERR_04367 ), pos );
351        }
352
353        char c = upGeneralizedTime.charAt( pos );
354        if ( c == 'Z' )
355        {
356            calendar.setTimeZone( GMT );
357            upTimeZoneFormat = TimeZoneFormat.Z;
358
359            if ( upGeneralizedTime.length() > pos + 1 )
360            {
361                throw new ParseException( I18n.err( I18n.ERR_04368 ), pos + 1 );
362            }
363        }
364        else if ( c == '+' || c == '-' )
365        {
366            StringBuilder sb = new StringBuilder( "GMT" );
367            sb.append( c );
368
369            String digits = getAllDigits( pos + 1 );
370            sb.append( digits );
371
372            if ( digits.length() == 2 && digits.matches( "^([01]\\d|2[0-3])$" ) )
373            {
374                TimeZone timeZone = TimeZone.getTimeZone( sb.toString() );
375                calendar.setTimeZone( timeZone );
376                upTimeZoneFormat = TimeZoneFormat.DIFF_HOUR;
377            }
378            else if ( digits.length() == 4 && digits.matches( "^([01]\\d|2[0-3])([0-5]\\d)$" ) )
379            {
380                TimeZone timeZone = TimeZone.getTimeZone( sb.toString() );
381                calendar.setTimeZone( timeZone );
382                upTimeZoneFormat = TimeZoneFormat.DIFF_HOUR_MINUTE;
383            }
384            else
385            {
386                throw new ParseException( I18n.err( I18n.ERR_04369 ), pos );
387            }
388
389            if ( upGeneralizedTime.length() > pos + 1 + digits.length() )
390            {
391                throw new ParseException( I18n.err( I18n.ERR_04370 ), pos + 1 + digits.length() );
392            }
393        }
394    }
395
396
397    private void parseFractionOfSecond() throws ParseException
398    {
399        parseFractionDelmiter( 14 );
400        String fraction = getFraction( 14 + 1 );
401        upFractionLength = fraction.length();
402
403        double fract = Double.parseDouble( "0." + fraction );
404        int millisecond = ( int ) Math.round( fract * 1000 );
405
406        calendar.set( Calendar.MILLISECOND, millisecond );
407    }
408
409
410    private void parseFractionOfMinute() throws ParseException
411    {
412        parseFractionDelmiter( 12 );
413        String fraction = getFraction( 12 + 1 );
414        upFractionLength = fraction.length();
415
416        double fract = Double.parseDouble( "0." + fraction );
417        int milliseconds = ( int ) Math.round( fract * 1000 * 60 );
418        int second = milliseconds / 1000;
419        int millisecond = milliseconds - ( second * 1000 );
420
421        calendar.set( Calendar.SECOND, second );
422        calendar.set( Calendar.MILLISECOND, millisecond );
423    }
424
425
426    private void parseFractionOfHour() throws ParseException
427    {
428        parseFractionDelmiter( 10 );
429        String fraction = getFraction( 10 + 1 );
430        upFractionLength = fraction.length();
431
432        double fract = Double.parseDouble( "0." + fraction );
433        int milliseconds = ( int ) Math.round( fract * 1000 * 60 * 60 );
434        int minute = milliseconds / ( 1000 * 60 );
435        int second = ( milliseconds - ( minute * 60 * 1000 ) ) / 1000;
436        int millisecond = milliseconds - ( minute * 60 * 1000 ) - ( second * 1000 );
437
438        calendar.set( Calendar.MINUTE, minute );
439        calendar.set( Calendar.SECOND, second );
440        calendar.set( Calendar.MILLISECOND, millisecond );
441    }
442
443
444    private void parseFractionDelmiter( int fractionDelimiterPos )
445    {
446        char c = upGeneralizedTime.charAt( fractionDelimiterPos );
447        upFractionDelimiter = c == '.' ? FractionDelimiter.DOT : FractionDelimiter.COMMA;
448    }
449
450
451    private String getFraction( int startIndex ) throws ParseException
452    {
453        String fraction = getAllDigits( startIndex );
454
455        // minimum one digit
456        if ( fraction.length() == 0 )
457        {
458            throw new ParseException( I18n.err( I18n.ERR_04371 ), startIndex );
459        }
460
461        return fraction;
462    }
463
464
465    private String getAllDigits( int startIndex )
466    {
467        StringBuilder sb = new StringBuilder();
468        while ( upGeneralizedTime.length() > startIndex )
469        {
470            char c = upGeneralizedTime.charAt( startIndex );
471            if ( '0' <= c && c <= '9' )
472            {
473                sb.append( c );
474                startIndex++;
475            }
476            else
477            {
478                break;
479            }
480        }
481        return sb.toString();
482    }
483
484
485    private void parseSecond() throws ParseException
486    {
487        // read minute
488        if ( upGeneralizedTime.length() < 14 )
489        {
490            throw new ParseException( I18n.err( I18n.ERR_04372 ), 12 );
491        }
492        try
493        {
494            int second = Integer.parseInt( upGeneralizedTime.substring( 12, 14 ) );
495            calendar.set( Calendar.SECOND, second );
496        }
497        catch ( NumberFormatException e )
498        {
499            throw new ParseException( I18n.err( I18n.ERR_04373 ), 12 );
500        }
501    }
502
503
504    private void parseMinute() throws ParseException
505    {
506        // read minute
507        if ( upGeneralizedTime.length() < 12 )
508        {
509            throw new ParseException( I18n.err( I18n.ERR_04374 ), 10 );
510        }
511        try
512        {
513            int minute = Integer.parseInt( upGeneralizedTime.substring( 10, 12 ) );
514            calendar.set( Calendar.MINUTE, minute );
515        }
516        catch ( NumberFormatException e )
517        {
518            throw new ParseException( I18n.err( I18n.ERR_04375 ), 10 );
519        }
520    }
521
522
523    private void parseHour() throws ParseException
524    {
525        if ( upGeneralizedTime.length() < 10 )
526        {
527            throw new ParseException( I18n.err( I18n.ERR_04376 ), 8 );
528        }
529        try
530        {
531            int hour = Integer.parseInt( upGeneralizedTime.substring( 8, 10 ) );
532            calendar.set( Calendar.HOUR_OF_DAY, hour );
533        }
534        catch ( NumberFormatException e )
535        {
536            throw new ParseException( I18n.err( I18n.ERR_04377 ), 8 );
537        }
538    }
539
540
541    private void parseDay() throws ParseException
542    {
543        if ( upGeneralizedTime.length() < 8 )
544        {
545            throw new ParseException( I18n.err( I18n.ERR_04378 ), 6 );
546        }
547        try
548        {
549            int day = Integer.parseInt( upGeneralizedTime.substring( 6, 8 ) );
550            calendar.set( Calendar.DAY_OF_MONTH, day );
551        }
552        catch ( NumberFormatException e )
553        {
554            throw new ParseException( I18n.err( I18n.ERR_04379 ), 6 );
555        }
556    }
557
558
559    private void parseMonth() throws ParseException
560    {
561        if ( upGeneralizedTime.length() < 6 )
562        {
563            throw new ParseException( I18n.err( I18n.ERR_04380 ), 4 );
564        }
565        try
566        {
567            int month = Integer.parseInt( upGeneralizedTime.substring( 4, 6 ) );
568            calendar.set( Calendar.MONTH, month - 1 );
569        }
570        catch ( NumberFormatException e )
571        {
572            throw new ParseException( I18n.err( I18n.ERR_04381 ), 4 );
573        }
574    }
575
576
577    private void parseYear() throws ParseException
578    {
579        if ( upGeneralizedTime.length() < 4 )
580        {
581            throw new ParseException( I18n.err( I18n.ERR_04382 ), 0 );
582        }
583        try
584        {
585            int year = Integer.parseInt( upGeneralizedTime.substring( 0, 4 ) );
586            calendar.set( Calendar.YEAR, year );
587        }
588        catch ( NumberFormatException e )
589        {
590            throw new ParseException( I18n.err( I18n.ERR_04383 ), 0 );
591        }
592    }
593
594
595    /**
596     * Returns the string representation of this generalized time. 
597     * This method uses the same format as the user provided format.
598     *
599     * @return the string representation of this generalized time
600     */
601    public String toGeneralizedTime()
602    {
603        return toGeneralizedTime( upFormat, upFractionDelimiter, upFractionLength, upTimeZoneFormat );
604    }
605
606
607    /**
608     * Returns the string representation of this generalized time.
609     * 
610     * @param format the target format
611     * @param fractionDelimiter the target fraction delimiter, may be null
612     * @param fractionLength the fraction length
613     * @param timeZoneFormat the target time zone format
614     * 
615     * @return the string
616     */
617    public String toGeneralizedTime( Format format, FractionDelimiter fractionDelimiter, int fractionLength,
618        TimeZoneFormat timeZoneFormat )
619    {
620        Calendar clonedCalendar = ( Calendar ) this.calendar.clone();
621        if ( timeZoneFormat == TimeZoneFormat.Z )
622        {
623            clonedCalendar.setTimeZone( GMT );
624        }
625
626        NumberFormat twoDigits = new DecimalFormat( "00" );
627        NumberFormat fourDigits = new DecimalFormat( "00" );
628        StringBuffer fractionFormat = new StringBuffer( "" );
629        for ( int i = 0; i < fractionLength && i < 3; i++ )
630        {
631            fractionFormat.append( "0" );
632        }
633
634        StringBuilder sb = new StringBuilder();
635        sb.append( fourDigits.format( clonedCalendar.get( Calendar.YEAR ) ) );
636        sb.append( twoDigits.format( clonedCalendar.get( Calendar.MONTH ) + 1 ) );
637        sb.append( twoDigits.format( clonedCalendar.get( Calendar.DAY_OF_MONTH ) ) );
638        sb.append( twoDigits.format( clonedCalendar.get( Calendar.HOUR_OF_DAY ) ) );
639
640        switch ( format )
641        {
642            case YEAR_MONTH_DAY_HOUR_MIN_SEC:
643                sb.append( twoDigits.format( clonedCalendar.get( Calendar.MINUTE ) ) );
644                sb.append( twoDigits.format( clonedCalendar.get( Calendar.SECOND ) ) );
645                break;
646
647            case YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION:
648                sb.append( twoDigits.format( clonedCalendar.get( Calendar.MINUTE ) ) );
649                sb.append( twoDigits.format( clonedCalendar.get( Calendar.SECOND ) ) );
650
651                NumberFormat fractionDigits = new DecimalFormat( fractionFormat.toString() );
652                sb.append( fractionDelimiter == FractionDelimiter.COMMA ? ',' : '.' );
653                sb.append( fractionDigits.format( clonedCalendar.get( Calendar.MILLISECOND ) ) );
654                break;
655
656            case YEAR_MONTH_DAY_HOUR_MIN:
657                sb.append( twoDigits.format( clonedCalendar.get( Calendar.MINUTE ) ) );
658                break;
659
660            case YEAR_MONTH_DAY_HOUR_MIN_FRACTION:
661                sb.append( twoDigits.format( clonedCalendar.get( Calendar.MINUTE ) ) );
662
663                // sec + millis => fraction of minute
664                double millisec = 1000 * clonedCalendar.get( Calendar.SECOND ) + clonedCalendar.get( Calendar.MILLISECOND );
665                double fraction = millisec / ( 1000 * 60 );
666                fractionDigits = new DecimalFormat( "0." + fractionFormat );
667                sb.append( fractionDelimiter == FractionDelimiter.COMMA ? ',' : '.' );
668                sb.append( fractionDigits.format( fraction ).substring( 2 ) );
669                break;
670
671            case YEAR_MONTH_DAY_HOUR_FRACTION:
672                // min + sec + millis => fraction of minute
673                millisec = 1000 * 60 * clonedCalendar.get( Calendar.MINUTE ) + 1000 * clonedCalendar.get( Calendar.SECOND )
674                    + clonedCalendar.get( Calendar.MILLISECOND );
675                fraction = millisec / ( 1000 * 60 * 60 );
676                fractionDigits = new DecimalFormat( "0." + fractionFormat );
677                sb.append( fractionDelimiter == FractionDelimiter.COMMA ? ',' : '.' );
678                sb.append( fractionDigits.format( fraction ).substring( 2 ) );
679
680                break;
681        }
682
683        if ( timeZoneFormat == TimeZoneFormat.Z && clonedCalendar.getTimeZone().hasSameRules( GMT ) )
684        {
685            sb.append( 'Z' );
686        }
687        else
688        {
689            TimeZone timeZone = clonedCalendar.getTimeZone();
690            int rawOffset = timeZone.getRawOffset();
691            sb.append( rawOffset < 0 ? '-' : '+' );
692
693            rawOffset = Math.abs( rawOffset );
694            int hour = rawOffset / ( 60 * 60 * 1000 );
695            int minute = ( rawOffset - ( hour * 60 * 60 * 1000 ) ) / ( 1000 * 60 );
696
697            if ( hour < 10 )
698            {
699                sb.append( '0' );
700            }
701            sb.append( hour );
702
703            if ( timeZoneFormat == TimeZoneFormat.DIFF_HOUR_MINUTE || timeZoneFormat == TimeZoneFormat.Z )
704            {
705                if ( minute < 10 )
706                {
707                    sb.append( '0' );
708                }
709                sb.append( minute );
710            }
711        }
712
713        return sb.toString();
714    }
715
716
717    /**
718     * Gets the calendar. It could be used to manipulate this 
719     * {@link GeneralizedTime} settings.
720     * 
721     * @return the calendar
722     */
723    public Calendar getCalendar()
724    {
725        return calendar;
726    }
727
728
729    @Override
730    public String toString()
731    {
732        return toGeneralizedTime();
733    }
734
735
736    @Override
737    public int hashCode()
738    {
739        final int prime = 31;
740        int result = 1;
741        result = prime * result + calendar.hashCode();
742        return result;
743    }
744
745
746    @Override
747    public boolean equals( Object obj )
748    {
749        if ( obj instanceof GeneralizedTime )
750        {
751            GeneralizedTime other = ( GeneralizedTime ) obj;
752            return calendar.equals( other.calendar );
753        }
754        else
755        {
756            return false;
757        }
758    }
759
760
761    /**
762     * Compares this GeneralizedTime object with the specified GeneralizedTime object.
763     * 
764     * @param other the other GeneralizedTime object
765     * 
766     * @return a negative integer, zero, or a positive integer as this object
767     *      is less than, equal to, or greater than the specified object.
768     * 
769     * @see java.lang.Comparable#compareTo(java.lang.Object)
770     */
771    public int compareTo( GeneralizedTime other )
772    {
773        return calendar.compareTo( other.calendar );
774    }
775
776}