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 }