    001/*
    002 * Copyright (c) 2009 The openGion Project.
    003 *
    004 * Licensed under the Apache License, Version 2.0 (the "License");
    005 * you may not use this file except in compliance with the License.
    006 * You may obtain a copy of the License at
    007 *
    008 *     http://www.apache.org/licenses/LICENSE-2.0
    009 *
    010 * Unless required by applicable law or agreed to in writing, software
    011 * distributed under the License is distributed on an "AS IS" BASIS,
    012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
    013 * either express or implied. See the License for the specific language
    014 * governing permissions and limitations under the License.
    015 */
    016package org.opengion.fukurou.util;
    017
    018import java.util.NoSuchElementException;
    019import java.util.Iterator;
    020
    021import static org.opengion.fukurou.system.HybsConst.CR;                         // 6.4.5.1 (2016/04/28)
    022
    023/**
    024 * CSVTokenizer は、CSVファイルのデータを順次分割する StringTokenizer と非常に
    025 * 良く似たクラスです。
    026 *
    027 * StringTokenizer では、デリミタが連続する場合も、１つのデリミタとするため、
    028 * データが存在しない場合の表現が出来ませんでした。(例えば、AA,BB,,DD など)
    029 * 現在では、StringTokenizerの代わりに、String#split(String)が推奨されており、
    030 * これなら、区切り文字を正規表現で指定できるため、両方の形式に対応できます。
    031 * ただし、ダブルクオート等で囲われたデータを塊として処理することが出来ません。
    032 * ( AA,BB,"cc,dd",EE などを、「AA」「BB」「cc,dd」「EE」に分割したい、など)
    033 *
    034 * この、CSVTokenizer クラスでは、データが存在しない場合もトークンとして返します。
    035 *
    036 * また、ダブルコーテーション("")で囲まれた範囲のデリミタは無視します。
    037 * ("") は、２重にすることで、エスケープできます。これにより、改行コードで分割した
    038 * １行の中に、("")が奇数の場合は、次の行は、続いていると判定できます。
    039 * ただし、デリミタとしては、文字列は指定できず、cher 1文字 しか指定できません。
    040 *
    041 * @og.rev 6.4.5.1 (2016/04/28) Iteratorインタフェース化。
    042 *
    043 * @version  4.0
    044 * @author   Kazuhiko Hasegawa
    045 * @since    JDK5.0,
    046 */
    047public class CSVTokenizer implements Iterator<String> {
    048        private int             currentPosition;
    049        private int             maxPosition;
    050        private String  str;
    051        private char    delimChar ;
    052        private boolean inQuoteFlag ;           // "" クオート処理を行うかどうか
    053
    054        /**
    055         * CSV形式の 文字列を解析する CSVTokenizer のインスタンスを作成する。
    056         *
    057         * @param str           CSV形式の文字列  改行コードを含まない。
    058         * @param delim         区切り文字(１文字のみ指定可)
    059         * @param inQuote       クオート処理を行うかどうか [true:行う/false:行わない]
    060         */
    061        public CSVTokenizer( final String str, final char delim, final boolean inQuote ) {
    062                currentPosition = 0;
    063                this.str        = str;
    064                maxPosition     = str.length();
    065                delimChar       = delim;
    066                inQuoteFlag     = inQuote;
    067        }
    068
    069        /**
    070         * CSV形式の 文字列を解析する CSVTokenizer のインスタンスを作成する。
    071         *
    072         * inQuote=true を初期値設定されます。
    073         *
    074         * @og.rev 6.4.5.1 (2016/04/28) Iteratorインタフェース化。
    075         *
    076         * @param str CSV形式の文字列  改行コードを含まない。
    077         * @param delim  区切り文字(１文字のみ指定可)
    078         */
    079        public CSVTokenizer( final String str, final char delim ) {
    080                this( str, delim, true );
    081        }
    082
    083        /**
    084         * CSV形式の 文字列を解析する CSVTokenizer のインスタンスを作成する。
    085         *
    086         * delim=',' , inQuote=true を初期値設定されます。
    087         *
    088         * @param str CSV形式の文字列  改行コードを含まない。
    089         */
    090        public CSVTokenizer( final String str ) {
    091                this( str, ',', true );
    092        }
    093
    094        /**
    095         * トークナイザの文字列で利用できるトークンがまだあるかどうかを判定します。
    096         * このメソッドが true を返す場合、それ以降の引数のない next() への
    097         * 呼び出しは適切にトークンを返します。
    098         *
    099         * @og.rev 6.4.5.1 (2016/04/28) Iteratorインタフェース化。
    100         *
    101         * @return  文字列内の現在の位置の後ろに 1 つ以上のトークンがある場合だけ true、そうでない場合は false
    102         */
    103        @Override
    104        public boolean hasNext() {
    105                final int newPosition = skipDelimiters(currentPosition);
    106                return newPosition <= maxPosition ;             // "<" だと末尾の項目を正しく処理できない
    107        }
    108
    109        /**
    110         * 文字列トークナイザから次のトークンを返します。
    111         *
    112         * @og.rev 5.2.0.0 (2010/09/01) トークンの前後が '"'である場合、"で囲われた文字列中の""は"に変換します。
    113         * @og.rev 6.4.5.1 (2016/04/28) Iteratorインタフェース化。
    114         *
    115         * @return      文字列トークナイザからの次のトークン
    116         * @throws      NoSuchElementException トークナイザの文字列にトークンが残っていない場合
    117        */
    118        @Override
    119        public String next() {
    120                // ">=" では末尾の項目を正しく処理できない。
    121                // 末尾の項目が空(カンマで1行が終わる)場合、例外が発生して
    122                // しまうので。
    123                if( currentPosition > maxPosition ) {
    124                        final String errMsg = "currentPosition=[" + currentPosition + "] , maxPosition=[" + maxPosition + "]" + CR
    125                                                                        + toString() ;
    126                        throw new NoSuchElementException( errMsg );
    127                }
    128
    129                // 3.5.4.7 (2004/02/06)
    130                int from = currentPosition;
    131                int to   = skipDelimiters( currentPosition );
    132                currentPosition = to + 1;
    133                // 5.2.0.0 (2010/09/01) トークンの前後が '"'である場合、"で囲われた文字列中の""は"に変換します。
    134                String rtn = null;
    135                // 3.5.5.8 (2004/05/20) トークンの前後が '"' なら、削除します。
    136                if( inQuoteFlag && from < maxPosition && from < to &&
    137                        str.charAt(from) == '"' && str.charAt(to-1) == '"' ) {
    138                        from++;
    139                        to--;
    140                        rtn = str.substring( from,to ).replace( "\"\"", "\"" );
    141                }
    142                else {
    143                        rtn = str.substring( from,to );
    144                }
    145
    146                return rtn;
    147        }
    148
    149        /**
    150         * 例外を生成せずにトークナイザの <code>next()</code> メソッドを呼び出せる
    151         * 回数を計算します。現在の位置は進みません。
    152         *
    153         * @return  現在の区切り文字を適用したときに文字列に残っているトークンの数
    154         * @see     CSVTokenizer#next()
    155         */
    156        public int countTokens() {
    157                int count = 1;
    158                int currpos = 0;
    159                while( (currpos = skipDelimiters(currpos)) < maxPosition ) {
    160                        currpos++;
    161                        count++;
    162                }
    163                return count;
    164        }
    165
    166        /**
    167         * 次のカンマがある位置を返す。
    168         * カンマが残っていない場合は skipDelimiters() == maxPosition となる。
    169         * また最後の項目が空の場合も skipDelimiters() == maxPosition となる。
    170         *
    171         * @param startPos 検索を開始する位置
    172         *
    173         * @return 次のカンマがある位置。カンマがない場合は、文字列の長さの値となる。
    174         */
    175        private int skipDelimiters( final int startPos ) {
    176                boolean inquote = false;
    177                int position = startPos;
    178                while( position < maxPosition ) {
    179                        final char ch = str.charAt(position);
    180                        if( !inquote && ch == delimChar ) {
    181                                break;
    182                        } else if( '"' == ch && inQuoteFlag ) {
    183                                inquote = !inquote;                             // "" クオート処理を行う
    184                        }
    185                        position++;
    186                }
    187                return position;
    188        }
    189
    190        /**
    191         * インスタンスの文字列表現を返す。
    192         *
    193         * @return インスタンスの文字列表現。
    194         * @og.rtnNotNull
    195         */
    196        @Override
    197        public String toString() {
    198                return "CSVTokenizer(" + str + ")";
    199        }
    200}