OSDN Git Service

OSDN.JP 対応
[jovsonz/Jovsonz.git] / src / main / java / jp / sourceforge / jovsonz / JsNumber.java
1 /*
2  * JSON number value
3  *
4  * License : The MIT License
5  * Copyright(c) 2009 olyutorskii
6  */
7
8 package jp.sourceforge.jovsonz;
9
10 import java.io.IOException;
11 import java.math.BigDecimal;
12 import java.math.BigInteger;
13 import java.math.MathContext;
14 import java.math.RoundingMode;
15
16 /**
17  * JSON NUMBER型Valueを表す。
18  * 整数、実数を含めた数値を反映する。
19  * <p>
20  * 10を基数とした{@link java.math.BigDecimal}を実装ベースとする。
21  * ※ IEEE754浮動小数ではない。
22  * </p>
23  * <p>(1)と(1.0)はスケール値によって区別される</p>
24  * <h1>表記例</h1>
25  * <pre>
26  * -43
27  * 0.56
28  * 3.23E-06
29  * </pre>
30  * @see java.math.BigDecimal
31  */
32 public class JsNumber
33         implements JsValue, Comparable<JsNumber> {
34
35     private static final MathContext DEF_MC =
36             new MathContext(0, RoundingMode.UNNECESSARY);
37
38     private static final String ERRMSG_INVFRAC =
39             "invalid fractional number";
40     private static final String ERRMSG_NONUMBER =
41             "no number";
42     private static final String ERRMSG_EXTRAZERO =
43             "extra zero found";
44
45     private final BigDecimal decimal;
46
47     /**
48      * コンストラクタ。
49      * @param val 初期整数値
50      */
51     public JsNumber(long val){
52         this(BigDecimal.valueOf(val));
53         return;
54     }
55
56     /**
57      * コンストラクタ。
58      * <p>
59      * {@link java.math.BigDecimal#valueOf(double)}と同等の丸めが行われる。
60      * (1.0/10.0)を渡すと0.1相当になる。
61      * 必要に応じて{@link java.math.BigDecimal}を
62      * 引数に持つコンストラクタと使い分けること。
63      * </p>
64      * @param val 初期実数値
65      * @see java.math.BigDecimal#valueOf(double)
66      */
67     public JsNumber(double val){
68         this(BigDecimal.valueOf(val));
69         return;
70     }
71
72     /**
73      * コンストラクタ。
74      * @param val 初期整数値
75      * @throws ArithmeticException 正確な結果を
76      * {@link java.math.BigDecimal}に納め切れない
77      */
78     public JsNumber(BigInteger val) throws ArithmeticException{
79         this(new BigDecimal(val, DEF_MC));
80         return;
81     }
82
83     /**
84      * コンストラクタ。
85      * 書式は{@link java.math.BigDecimal#BigDecimal(String)}に準ずる。
86      * @param val 初期数値の文字列表記
87      * @throws NumberFormatException 不正な数値表記
88      * @throws ArithmeticException 正確な結果を
89      * {@link java.math.BigDecimal}に納め切れない
90      * @see java.math.BigDecimal#BigDecimal(String)
91      */
92     public JsNumber(CharSequence val)
93             throws NumberFormatException, ArithmeticException{
94         this(new BigDecimal(val.toString(), DEF_MC));
95         return;
96     }
97
98     /**
99      * コンストラクタ。
100      * @param val 初期数値
101      * @throws NullPointerException 引数がnull
102      */
103     public JsNumber(BigDecimal val) throws NullPointerException{
104         super();
105         if(val == null) throw new NullPointerException();
106         this.decimal = val;
107         return;
108     }
109
110     /**
111      * 任意の文字がUnicodeのBasic-Latinの数字か否か判定する。
112      * @param ch 文字
113      * @return 数字ならtrue
114      * @see java.lang.Character#isDigit(char)
115      */
116     public static boolean isLatinDigit(char ch){
117         if('0' <= ch && ch <= '9') return true;
118         return false;
119     }
120
121     /**
122      * 文字ソースから符号付きの数字並びを読み込む。
123      * 先頭'+'符号は読み飛ばされる。
124      * 冒頭のゼロ'0'に続く数字を許すか否か指定が可能。
125      * <p>NUMBER型表記の整数部、小数部、指数部読み込みの下請けメソッド。</p>
126      * @param source 文字列ソース
127      * @param app 出力先
128      * @param allowZeroTrail 冒頭のゼロ'0'に続く数字を許すならtrue
129      * @return 引数と同じ出力先
130      * @throws IOException 入出力エラー
131      * @throws JsParseException 不正な書式もしくは意図しない入力終了
132      */
133     private static Appendable appendDigitText(JsonSource source,
134                                               Appendable app,
135                                               boolean allowZeroTrail)
136             throws IOException, JsParseException{
137         char head = source.readOrDie();
138         if     (head == '-') app.append('-');
139         else if(head != '+') source.unread(head);
140
141         boolean hasAppended = false;
142         boolean zeroStarted = false;    // 先頭は0か
143         for(;;){
144             if( ! source.hasMore() && hasAppended) break;
145
146             char readedCh = source.readOrDie();
147
148             if( ! isLatinDigit(readedCh) ){
149                 if( ! hasAppended ){
150                     throw new JsParseException(ERRMSG_NONUMBER,
151                                                source.getLineNumber() );
152                 }
153                 source.unread(readedCh);
154                 break;
155             }
156
157             if(hasAppended){
158                 if(zeroStarted && ! allowZeroTrail){
159                     throw new JsParseException(ERRMSG_EXTRAZERO,
160                                                source.getLineNumber() );
161                 }
162             }else{                       // 1st char
163                 if(readedCh == '0'){
164                     zeroStarted = true;
165                 }
166             }
167
168             app.append(readedCh);
169             hasAppended = true;
170         }
171
172         return app;
173     }
174
175     /**
176      * 文字ソースから、ピリオド「.」で始まるNUMBER型小数部を読み込む。
177      * 小数部がなければなにもせずに戻る。
178      * @param source 文字列ソース
179      * @param app 出力先
180      * @return 引数と同じ出力先
181      * @throws IOException 入出力エラー
182      * @throws JsParseException 不正な書式もしくは意図しない入力終了
183      */
184     private static Appendable appendFractionPart(JsonSource source,
185                                                  Appendable app )
186             throws IOException, JsParseException{
187         if( ! source.hasMore() ) return app;
188
189         char chData;
190
191         chData = source.readOrDie();
192         if(chData != '.'){
193             source.unread(chData);
194             return app;
195         }
196
197         app.append(".");
198
199         boolean hasAppended = false;
200         for(;;){
201             if( ! source.hasMore() && hasAppended) break;
202
203             chData = source.readOrDie();
204
205             if( ! isLatinDigit(chData) ){
206                 if( ! hasAppended ){
207                     throw new JsParseException(ERRMSG_INVFRAC,
208                                                source.getLineNumber());
209                 }
210                 source.unread(chData);
211                 break;
212             }
213
214             app.append(chData);
215             hasAppended = true;
216         }
217
218         return app;
219     }
220
221     /**
222      * 文字ソースから「e」もしくは「E」で始まるNUMBER型指数部を読み込む。
223      * 指数部がなければなにもせずに戻る。
224      * @param source 文字列ソース
225      * @param app 出力先
226      * @return 引数と同じ出力先
227      * @throws IOException 入出力エラー
228      * @throws JsParseException 不正な書式もしくは意図しない入力終了
229      */
230     private static Appendable appendExpPart(JsonSource source,
231                                             Appendable app )
232             throws IOException, JsParseException{
233         if( ! source.hasMore() ) return app;
234
235         char chData = source.readOrDie();
236         if(chData != 'e' && chData != 'E'){
237             source.unread(chData);
238             return app;
239         }
240
241         app.append('E');
242
243         appendDigitText(source, app, true);
244
245         return app;
246     }
247
248     /**
249      * JSON文字列ソースからNUMBER型Valueを読み込む。
250      * 別型の可能性のある先頭文字を読み込んだ場合、
251      * ソースに文字を読み戻した後nullが返される。
252      * @param source 文字列ソース
253      * @return NUMBER型Value。別型の可能性がある場合はnull。
254      * @throws IOException 入力エラー
255      * @throws JsParseException 不正な表記もしくは意図しない入力終了
256      */
257     static JsNumber parseNumber(JsonSource source)
258             throws IOException, JsParseException{
259         char charHead = source.readOrDie();
260         source.unread(charHead);
261         if( charHead != '-' && ! JsNumber.isLatinDigit(charHead) ){
262             return null;
263         }
264
265         StringBuilder numText = new StringBuilder();
266
267         appendDigitText   (source, numText, false);
268         appendFractionPart(source, numText);
269         appendExpPart     (source, numText);
270
271         JsNumber result = new JsNumber(numText);
272
273         return result;
274     }
275
276     /**
277      * {@inheritDoc}
278      * 常に{@link JsTypes#NUMBER}を返す。
279      * @return {@inheritDoc}
280      */
281     @Override
282     public JsTypes getJsTypes(){
283         return JsTypes.NUMBER;
284     }
285
286     /**
287      * 各種構造の出現をビジターに通知する。
288      * この実装ではthisの出現のみを通知する。
289      * @param visitor {@inheritDoc}
290      * @throws JsVisitException {@inheritDoc}
291      */
292     @Override
293     public void traverse(ValueVisitor visitor)
294             throws JsVisitException{
295         visitor.visitValue(this);
296         return;
297     }
298
299     /**
300      * {@inheritDoc}
301      * ハッシュ値を返す。
302      * {@link java.math.BigDecimal#hashCode()}と同じ値を返す。
303      * @return {@inheritDoc}
304      * @see java.math.BigDecimal#hashCode()
305      */
306     @Override
307     public int hashCode(){
308         return this.decimal.hashCode();
309     }
310
311     /**
312      * {@inheritDoc}
313      * 等価判定を行う。
314      * {@link java.math.BigDecimal#equals(Object)}と同等の判断が行われる。
315      * 「1.2」と「0.12E+1」など、
316      * スケールの一致しない値は異なる値と見なされる。
317      * @param obj {@inheritDoc}
318      * @return {@inheritDoc}
319      * @see java.math.BigDecimal#equals(Object)
320      */
321     @Override
322     public boolean equals(Object obj){
323         if(this == obj) return true;
324         if( ! (obj instanceof JsNumber) ) return false;
325         JsNumber number = (JsNumber) obj;
326         return this.decimal.equals(number.decimal);
327     }
328
329     /**
330      * {@inheritDoc}
331      * NUMBER型Valueを昇順に順序付ける。
332      * 「1.2」と「0.12E+1」など、スケールが異なっても値が同じであれば
333      * 等しい値と見なされる。
334      * @param value {@inheritDoc}
335      * @return {@inheritDoc}
336      * @see java.math.BigDecimal#compareTo(BigDecimal)
337      */
338     @Override
339     public int compareTo(JsNumber value){
340         if(this == value) return 0;
341         return this.decimal.compareTo(value.decimal);
342     }
343
344     /**
345      * int型の数値を返す。
346      * 情報が失われる可能性がある。
347      * @return int型数値
348      * @see java.lang.Number#intValue()
349      * @see java.math.BigDecimal#intValue()
350      */
351     public int intValue(){
352         return this.decimal.intValue();
353     }
354
355     /**
356      * long型の数値を返す。
357      * 情報が失われる可能性がある。
358      * @return long型数値
359      * @see java.lang.Number#longValue()
360      * @see java.math.BigDecimal#longValue()
361      */
362     public long longValue(){
363         return this.decimal.longValue();
364     }
365
366     /**
367      * float型の数値を返す。
368      * 情報が失われる可能性がある。
369      * @return float型数値
370      * @see java.lang.Number#floatValue()
371      * @see java.math.BigDecimal#floatValue()
372      */
373     public float floatValue(){
374         return this.decimal.floatValue();
375     }
376
377     /**
378      * double型の数値を返す。
379      * 情報が失われる可能性がある。
380      * @return double型数値
381      * @see java.lang.Number#doubleValue()
382      * @see java.math.BigDecimal#doubleValue()
383      */
384     public double doubleValue(){
385         return this.decimal.doubleValue();
386     }
387
388     /**
389      * {@link java.math.BigDecimal}型の数値表現を返す。
390      * @return BigDecimal型数値
391      */
392     public BigDecimal decimalValue(){
393         return this.decimal;
394     }
395
396     /**
397      * スケール値を返す。
398      * このインスタンスが整数文字列表記に由来する場合、
399      * スケール値は0になるはず。
400      *
401      * <ul>
402      * <li>"99"のスケール値は0
403      * <li>"99.0"のスケール値は1
404      * <li>"99.01"のスケール値は2
405      * <li>"99E+3"のスケール値は-3
406      * <li>"99.0E+3"のスケール値は-2
407      * </ul>
408      *
409      * @return スケール値
410      * @see java.math.BigDecimal#scale()
411      */
412     public int scale(){
413         return this.decimal.scale();
414     }
415
416     /**
417      * 文字列表現を返す。
418      * {@link java.math.BigDecimal#toString()}に準ずる。
419      * JSON表記の一部としての利用も可能。
420      * @return {@inheritDoc}
421      */
422     @Override
423     public String toString(){
424         return this.decimal.toString();
425     }
426
427 }