001    /*
002     * Copyright (c) 2006 Henri Sivonen
003     *
004     * Permission is hereby granted, free of charge, to any person obtaining a 
005     * copy of this software and associated documentation files (the "Software"), 
006     * to deal in the Software without restriction, including without limitation 
007     * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
008     * and/or sell copies of the Software, and to permit persons to whom the 
009     * Software is furnished to do so, subject to the following conditions:
010     *
011     * The above copyright notice and this permission notice shall be included in 
012     * all copies or substantial portions of the Software.
013     *
014     * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
015     * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
016     * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
017     * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
018     * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
019     * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
020     * DEALINGS IN THE SOFTWARE.
021     */
022    
023    package org.whattf.datatype;
024    
025    import org.relaxng.datatype.DatatypeException;
026    
027    import com.ibm.icu.lang.UCharacter;
028    import com.ibm.icu.text.UnicodeSet;
029    
030    public final class Ratio extends AbstractDatatype {
031    
032        /**
033         * The singleton instance.
034         */
035        public static final Ratio THE_INSTANCE = new Ratio();
036    
037        @SuppressWarnings("deprecation")
038        private static final UnicodeSet ZS = (UnicodeSet) new UnicodeSet("[:Zs:]").freeze();
039    
040        private Ratio() {
041            super();
042        }
043    
044        @Override
045        public void checkValid(CharSequence literal) throws DatatypeException {
046            int len = literal.length();
047            if (len == 0) {
048                throw new DatatypeException("Empty literal.");
049            }
050            int pos = 0;
051            pos = findANumber(literal, pos);
052            pos = skipZs(literal, pos);
053            if (pos == len) {
054                throw new DatatypeException("Premature end of literal\u2014neither a denominator nor a second number was found.");
055            }
056            if (isDenominator(literal.charAt(pos))) {
057                while (pos < len) {
058                    char c = literal.charAt(pos);
059                    if (c >= '0' && c <= '9') {
060                        throw new DatatypeException("Found digits after denominator.");
061                    }
062                    pos++;
063                }
064                return;
065            } else {
066                pos = findANumber(literal, pos);
067                pos = skipZs(literal, pos); // REVISIT this step not in spec
068                if (pos == len) {
069                    return;
070                }
071                if (isDenominator(literal.charAt(pos))) {
072                    throw new DatatypeException("Found a denominator after the second number.");
073                }
074                while (pos < len) {
075                    char c = literal.charAt(pos);
076                    if (c >= '0' && c <= '9') {
077                        throw new DatatypeException("Found digits after the second number.");
078                    }
079                    pos++;
080                }
081                return;            
082            }
083        }
084    
085        private boolean isDenominator(char c) {
086            switch (c) {
087                case '\u0025':
088                case '\u066A':
089                case '\uFE6A':
090                case '\uFF05':
091                case '\u2030':
092                case '\u2031':                
093                    return true;
094                default:
095                    return false;
096            }
097        }
098    
099        private int skipZs(CharSequence literal, int pos) throws DatatypeException {
100            // there are no astral Zs characters in Unicode 5.0.0, but let's be
101            // forward-compatible
102            int len = literal.length();
103            char prev = '\u0000';
104            while (pos < len) {
105                char c = literal.charAt(pos);
106                if (!UCharacter.isHighSurrogate(c)) {
107                    if (UCharacter.isLowSurrogate(c)) {
108                        if (!UCharacter.isHighSurrogate(prev)) {
109                            throw new DatatypeException("Bad UTF-16!");
110                        }
111                        if (!ZS.contains(UCharacter.getCodePoint(prev, c))) {
112                            return pos;
113                        }
114                    } else {
115                        if (!ZS.contains(c)) {
116                            return pos;
117                        }
118                    }
119                }
120                prev = c;
121                pos++;
122            }
123            return pos;
124        }
125    
126        private int findANumber(CharSequence literal, int pos)
127                throws DatatypeException {
128            boolean pointSeen = false;
129            boolean collectingNumber = false;
130            boolean lastWasPoint = false;
131            int len = literal.length();
132            while (pos < len) {
133                char c = literal.charAt(pos);
134                if (c == '.') {
135                    if (pointSeen) {
136                        throw new DatatypeException(
137                                "More than one decimal point in a number.");
138                    }
139                    pointSeen = true;
140                    lastWasPoint = true;
141                    collectingNumber = true;
142                } else if (c >= '0' && c <= '9') {
143                    collectingNumber = true;
144                    lastWasPoint = false;
145                } else {
146                    if (collectingNumber) {
147                        if (lastWasPoint) {
148                            throw new DatatypeException(
149                                    "A decimal point was not followed by a digit.");
150                        }
151                        return pos;
152                    }
153                }
154                pos++;
155            }
156            if (!collectingNumber) {
157                throw new DatatypeException(
158                        "Expected a number but did not find one.");
159            }
160            if (lastWasPoint) {
161                throw new DatatypeException(
162                        "A decimal point was not followed by a digit.");
163            }
164            return pos;
165        }
166    
167    }