001/*
002 *  jDTAUS Banking API
003 *  Copyright (C) 2005 Christian Schulte
004 *  <cs@schulte.it>
005 *
006 *  This library is free software; you can redistribute it and/or
007 *  modify it under the terms of the GNU Lesser General Public
008 *  License as published by the Free Software Foundation; either
009 *  version 2.1 of the License, or any later version.
010 *
011 *  This library is distributed in the hope that it will be useful,
012 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
013 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 *  Lesser General Public License for more details.
015 *
016 *  You should have received a copy of the GNU Lesser General Public
017 *  License along with this library; if not, write to the Free Software
018 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
019 *
020 */
021package org.jdtaus.banking;
022
023import java.lang.ref.Reference;
024import java.lang.ref.SoftReference;
025import java.text.DecimalFormat;
026import java.text.ParseException;
027import java.text.ParsePosition;
028import java.util.Collections;
029import java.util.HashMap;
030import java.util.Map;
031
032/**
033 * Unique identifier to a particular bank account at a german bank.
034 * <p>A Kontonummer is a positive integer with a maximum of ten digits.</p>
035 *
036 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
037 * @version $JDTAUS: Kontonummer.java 8661 2012-09-27 11:29:58Z schulte $
038 */
039public final class Kontonummer extends Number implements Comparable
040{
041
042    /**
043     * Constant for the electronic format of a Kontonummer.
044     * <p>The electronic format of a Kontonummer is a ten digit number with leading zeros omitted (e.g. 6789).</p>
045     */
046    public static final int ELECTRONIC_FORMAT = 4001;
047
048    /**
049     * Constant for the letter format of a Kontonummer.
050     * <p>The letter format of a Kontonummer is a ten digit number with leading zeros omitted separated by spaces
051     * between the first three digits and the second three digits, the second three digits and the third three digits,
052     * and between the third three digits and the lastdigit (e.g. 123 456 789 0).</p>
053     */
054    public static final int LETTER_FORMAT = 4002;
055
056    /** Maximum number of digits of a Kontonummer. */
057    public static final int MAX_DIGITS = 10;
058
059    /** Maximum number of characters of a Kontonummer. */
060    public static final int MAX_CHARACTERS = 13;
061
062    /** {@code 10^0..10^9}. */
063    private static final double[] EXP10 =
064    {
065        1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000,
066        1000000000
067    };
068
069    /** Serial version UID for backwards compatibility with 1.0.x classes. */
070    private static final long serialVersionUID = 3117245365189973632L;
071
072    /** Used to cache instances. */
073    private static volatile Reference cacheReference = new SoftReference( null );
074
075    /**
076     * German account code.
077     * @serial
078     */
079    private long kto;
080
081    /**
082     * Creates a new {@code Kontonummer} instance.
083     *
084     * @param accountCode The number to create an instance from.
085     *
086     * @throws IllegalArgumentException if {@code accountCode} is negative, zero or greater than 9999999999.
087     *
088     * @see #checkKontonummer(Number)
089     */
090    private Kontonummer( final Number accountCode )
091    {
092        if ( !Kontonummer.checkKontonummer( accountCode ) )
093        {
094            throw new IllegalArgumentException( accountCode.toString() );
095        }
096
097        this.kto = accountCode.longValue();
098    }
099
100    /**
101     * Parses text from a string to produce a {@code Kontonummer}.
102     * <p>The method attempts to parse text starting at the index given by {@code pos}. If parsing succeeds, then the
103     * index of {@code pos} is updated to the index after the last character used (parsing does not necessarily use all
104     * characters up to the end of the string), and the parsed value is returned. The updated {@code pos} can be used to
105     * indicate the starting point for the next call to this method.</p>
106     *
107     * @param accountCode A Kontonummer in either electronic or letter format.
108     * @param pos A {@code ParsePosition} object with index and error index information as described above.
109     *
110     * @return The parsed value, or {@code null} if the parse fails.
111     *
112     * @throws NullPointerException if either {@code accountCode} or {@code pos} is {@code null}.
113     */
114    public static Kontonummer parse( final String accountCode, final ParsePosition pos )
115    {
116        if ( accountCode == null )
117        {
118            throw new NullPointerException( "accountCode" );
119        }
120        if ( pos == null )
121        {
122            throw new NullPointerException( "pos" );
123        }
124
125        Kontonummer ret = null;
126        boolean sawSpace = false;
127        boolean failed = false;
128        final ParsePosition fmtPos = new ParsePosition( 0 );
129        final int len = accountCode.length();
130        final int startIndex = pos.getIndex();
131        final int maxIndex = startIndex + MAX_CHARACTERS;
132        final StringBuffer digits = new StringBuffer( MAX_DIGITS );
133        int mode = ELECTRONIC_FORMAT;
134        int part = 0;
135        int partStart = 0;
136        int partEnd = 2;
137        int digit = 0;
138        int i = startIndex;
139
140        for ( ; i < len && i < maxIndex && digits.length() < MAX_DIGITS; i++ )
141        {
142            final char c = accountCode.charAt( i );
143
144            if ( Character.isDigit( c ) )
145            {
146                sawSpace = false;
147
148                if ( mode == LETTER_FORMAT )
149                {
150                    if ( digit < partStart || digit > partEnd )
151                    {
152                        failed = true;
153                    }
154                    else
155                    {
156                        digits.append( c );
157                    }
158                }
159                else
160                {
161                    digits.append( c );
162                }
163
164                digit++;
165            }
166            else if ( c == ' ' )
167            {
168                if ( sawSpace || i == startIndex || ( mode == ELECTRONIC_FORMAT && digit != 3 ) )
169                {
170                    failed = true;
171                }
172                else
173                {
174                    mode = LETTER_FORMAT;
175                    switch ( part )
176                    {
177                        case 0:
178                            partStart = 3;
179                            partEnd = 5;
180                            break;
181                        case 1:
182                            partStart = 6;
183                            partEnd = 8;
184                            break;
185                        case 2:
186                            partStart = 9;
187                            partEnd = 9;
188                            break;
189                        default:
190                            failed = true;
191                            break;
192                    }
193                    part++;
194
195                    if ( digit < partStart || digit > partEnd )
196                    {
197                        failed = true;
198                    }
199                }
200
201                sawSpace = true;
202            }
203            else
204            {
205                failed = true;
206            }
207
208            if ( failed )
209            {
210                pos.setErrorIndex( i );
211                break;
212            }
213        }
214
215        if ( !failed )
216        {
217            final Number num = new DecimalFormat( "##########" ).parse( digits.toString(), fmtPos );
218
219            if ( num != null && fmtPos.getErrorIndex() == -1 )
220            {
221                final String key = num.toString();
222                ret = (Kontonummer) getCache().get( key );
223
224                if ( ret == null )
225                {
226                    if ( !Kontonummer.checkKontonummer( num ) )
227                    {
228                        pos.setErrorIndex( startIndex );
229                        ret = null;
230                    }
231                    else
232                    {
233                        pos.setIndex( i );
234                        ret = new Kontonummer( num );
235                        getCache().put( key, ret );
236                    }
237                }
238                else
239                {
240                    pos.setIndex( i );
241                }
242            }
243            else
244            {
245                pos.setErrorIndex( startIndex );
246            }
247        }
248
249        return ret;
250    }
251
252    /**
253     * Parses text from the beginning of the given string to produce a {@code Kontonummer}.
254     * <p>Unlike the {@link #parse(String, ParsePosition)} method this method throws a {@code ParseException} if
255     * {@code accountCode} cannot be parsed or is of invalid length.</p>
256     *
257     * @param accountCode A Kontonummer in either electronic or letter format.
258     *
259     * @return The parsed value.
260     *
261     * @throws NullPointerException if {@code accountCode} is {@code null}.
262     * @throws ParseException if the parse fails or {@code accountCode} is of invalid length.
263     */
264    public static Kontonummer parse( final String accountCode ) throws ParseException
265    {
266        if ( accountCode == null )
267        {
268            throw new NullPointerException( "accountCode" );
269        }
270
271        Kontonummer kto = (Kontonummer) getCache().get( accountCode );
272        if ( kto == null )
273        {
274            final ParsePosition pos = new ParsePosition( 0 );
275            kto = Kontonummer.parse( accountCode, pos );
276            if ( kto == null || pos.getErrorIndex() != -1 || pos.getIndex() < accountCode.length() )
277            {
278                throw new ParseException( accountCode, pos.getErrorIndex() != -1 ? pos.getErrorIndex() : pos.getIndex() );
279            }
280            else
281            {
282                getCache().put( accountCode, kto );
283            }
284        }
285
286        return kto;
287    }
288
289    /**
290     * Returns an instance for the Kontonummer identified by the given number.
291     *
292     * @param accountCode A number identifying a Kontonummer.
293     *
294     * @return An instance for {@code accountCode}.
295     *
296     * @throws NullPointerException if {@code accountCode} is {@code null}.
297     * @throws IllegalArgumentException if {@code accountCode} is negative, zero or greater than 9999999999.
298     *
299     * @see #checkKontonummer(Number)
300     */
301    public static Kontonummer valueOf( final Number accountCode )
302    {
303        if ( accountCode == null )
304        {
305            throw new NullPointerException( "accountCode" );
306        }
307
308        final String key = accountCode.toString();
309        Kontonummer ret = (Kontonummer) getCache().get( key );
310        if ( ret == null )
311        {
312            ret = new Kontonummer( accountCode );
313            getCache().put( key, ret );
314        }
315
316        return ret;
317    }
318
319    /**
320     * Parses text from the beginning of the given string to produce a {@code Kontonummer}.
321     * <p>Unlike the {@link #parse(String)} method this method throws an {@code IllegalArgumentException} if
322     * {@code accountCode} cannot be parsed or is of invalid length.</p>
323     *
324     * @param accountCode A Kontonummer in either electronic or letter format.
325     *
326     * @return The parsed value.
327     *
328     * @throws NullPointerException if {@code accountCode} is {@code null}.
329     * @throws IllegalArgumentException if the parse fails or {@code accountCode} is of invalid length.
330     */
331    public static Kontonummer valueOf( final String accountCode )
332    {
333        try
334        {
335            return Kontonummer.parse( accountCode );
336        }
337        catch ( final ParseException e )
338        {
339            throw (IllegalArgumentException) new IllegalArgumentException( accountCode ).initCause( e );
340        }
341    }
342
343    /**
344     * Checks a given number to conform to a Kontonummer.
345     *
346     * @param accountCode The number to check.
347     *
348     * @return {@code true} if {@code accountCode} is a valid Kontonummer; {@code false} if not.
349     */
350    public static boolean checkKontonummer( final Number accountCode )
351    {
352        boolean valid = accountCode != null;
353
354        if ( valid )
355        {
356            final long num = accountCode.longValue();
357            valid = num > 0L && num < 10000000000L;
358        }
359
360        return valid;
361    }
362
363    /**
364     * Returns this Kontonummer as an int value.
365     *
366     * @return This Kontonummer as an int value.
367     */
368    public int intValue()
369    {
370        return (int) this.kto;
371    }
372
373    /**
374     * Returns this Kontonummer as a long value.
375     *
376     * @return This Kontonummer as a long value.
377     */
378    public long longValue()
379    {
380        return this.kto;
381    }
382
383    /**
384     * Returns this Kontonummer as a float value.
385     *
386     * @return This Kontonummer as a float value.
387     */
388    public float floatValue()
389    {
390        return this.kto;
391    }
392
393    /**
394     * Returns this Kontonummer as a double value.
395     *
396     * @return This Kontonummer as a double value.
397     */
398    public double doubleValue()
399    {
400        return this.kto;
401    }
402
403    /**
404     * Formats a Kontonummer and appends the resulting text to the given string buffer.
405     *
406     * @param style The style to use ({@code ELECTRONIC_FORMAT} or {@code LETTER_FORMAT}).
407     * @param toAppendTo The buffer to which the formatted text is to be appended.
408     *
409     * @return The value passed in as {@code toAppendTo}.
410     *
411     * @throws NullPointerException if {@code toAppendTo} is {@code null}.
412     * @throws IllegalArgumentException if {@code style} is neither {@code ELECTRONIC_FORMAT} nor {@code LETTER_FORMAT}.
413     *
414     * @see #ELECTRONIC_FORMAT
415     * @see #LETTER_FORMAT
416     */
417    public StringBuffer format( final int style, final StringBuffer toAppendTo )
418    {
419        if ( toAppendTo == null )
420        {
421            throw new NullPointerException( "toAppendTo" );
422        }
423        if ( style != Kontonummer.ELECTRONIC_FORMAT && style != Kontonummer.LETTER_FORMAT )
424        {
425            throw new IllegalArgumentException( Integer.toString( style ) );
426        }
427
428        final int[] digits = Kontonummer.toDigits( this.kto );
429        for ( int i = digits.length - 1, lastDigit = 0; i >= 0; i-- )
430        {
431            if ( digits[i] != 0 || lastDigit > 0 )
432            {
433                toAppendTo.append( digits[i] );
434                lastDigit++;
435            }
436
437            if ( style == Kontonummer.LETTER_FORMAT && ( lastDigit == 3 || lastDigit == 6 || lastDigit == 9 ) )
438            {
439                toAppendTo.append( ' ' );
440            }
441        }
442
443        return toAppendTo;
444    }
445
446    /**
447     * Formats a Kontonummer to produce a string. Same as
448     * <blockquote>
449     * {@link #format(int, StringBuffer) format<code>(style, new StringBuffer()).toString()</code>}
450     * </blockquote>
451     *
452     * @param style The style to use ({@code ELECTRONIC_FORMAT} or {@code LETTER_FORMAT}).
453     *
454     * @return The formatted string.
455     *
456     * @throws IllegalArgumentException if {@code style} is neither {@code ELECTRONIC_FORMAT} nor {@code LETTER_FORMAT}.
457     *
458     * @see #ELECTRONIC_FORMAT
459     * @see #LETTER_FORMAT
460     */
461    public String format( final int style )
462    {
463        return this.format( style, new StringBuffer() ).toString();
464    }
465
466    /**
467     * Formats a Kontonummer to produce a string. Same as
468     * <blockquote>
469     * {@link #format(int) kontonummer.format(ELECTRONIC_FORMAT)}
470     * </blockquote>
471     *
472     * @param kontonummer The {@code Kontonummer} instance to format.
473     *
474     * @return The formatted string.
475     *
476     * @throws NullPointerException if {@code kontonummer} is {@code null}.
477     */
478    public static String toString( final Kontonummer kontonummer )
479    {
480        if ( kontonummer == null )
481        {
482            throw new NullPointerException( "kontonummer" );
483        }
484
485        return kontonummer.format( ELECTRONIC_FORMAT );
486    }
487
488    /**
489     * Creates an array holding the digits of {@code number}.
490     *
491     * @param number The number to return the digits for.
492     *
493     * @return An array holding the digits of {@code number}.
494     */
495    private static int[] toDigits( final long number )
496    {
497        int i;
498        int j;
499        long subst;
500        final int[] ret = new int[ MAX_DIGITS ];
501
502        for ( i = MAX_DIGITS - 1; i >= 0; i-- )
503        {
504            for ( j = i + 1, subst = 0L; j < MAX_DIGITS; j++ )
505            {
506                subst += ret[j] * EXP10[j];
507            }
508            ret[i] = (int) Math.floor( ( number - subst ) / EXP10[i] );
509        }
510
511        return ret;
512    }
513
514    /**
515     * Creates a string representing the properties of the instance.
516     *
517     * @return A string representing the properties of the instance.
518     */
519    private String internalString()
520    {
521        return new StringBuffer( 500 ).append( "{accountCode=" ).append( this.kto ).append( '}' ).toString();
522    }
523
524    /**
525     * Gets the current cache instance.
526     *
527     * @return Current cache instance.
528     */
529    private static Map getCache()
530    {
531        Map cache = (Map) cacheReference.get();
532        if ( cache == null )
533        {
534            cache = Collections.synchronizedMap( new HashMap( 1024 ) );
535            cacheReference = new SoftReference( cache );
536        }
537
538        return cache;
539    }
540
541    /**
542     * Compares this object with the specified object for order. Returns a negative integer, zero, or a positive integer
543     * as this object is less than, equal to, or greater than the specified object.<p>
544     *
545     * @param o The Object to be compared.
546     * @return A negative integer, zero, or a positive integer as this object is less than, equal to, or greater than
547     * the specified object.
548     *
549     * @throws NullPointerException if {@code o} is {@code null}.
550     * @throws ClassCastException if the specified object's type prevents it from being compared to this Object.
551     */
552    public int compareTo( final Object o )
553    {
554        if ( o == null )
555        {
556            throw new NullPointerException( "o" );
557        }
558        if ( !( o instanceof Kontonummer ) )
559        {
560            throw new ClassCastException( o.getClass().getName() );
561        }
562
563        int result = 0;
564        final Kontonummer that = (Kontonummer) o;
565
566        if ( !this.equals( that ) )
567        {
568            result = this.kto > that.kto ? 1 : -1;
569        }
570
571        return result;
572    }
573
574    /**
575     * Indicates whether some other object is equal to this one.
576     *
577     * @param o The reference object with which to compare.
578     *
579     * @return {@code true} if this object is the same as {@code o}; {@code false} otherwise.
580     */
581    public boolean equals( final Object o )
582    {
583        boolean equal = o == this;
584
585        if ( !equal && o instanceof Kontonummer )
586        {
587            equal = this.kto == ( (Kontonummer) o ).kto;
588        }
589
590        return equal;
591    }
592
593    /**
594     * Returns a hash code value for this object.
595     *
596     * @return A hash code value for this object.
597     */
598    public int hashCode()
599    {
600        return (int) ( this.kto ^ ( this.kto >>> 32 ) );
601    }
602
603    /**
604     * Returns a string representation of the object.
605     *
606     * @return A string representation of the object.
607     */
608    public String toString()
609    {
610        return super.toString() + this.internalString();
611    }
612
613}