ICU-13574 Basic parsing tests are passing on the pieces of code written so far, DecimalMatcher and MinusSignMatcher.

X-SVN-Rev: 40872
This commit is contained in:
Shane Carr 2018-02-08 09:59:35 +00:00
parent 31a4dfe3e4
commit 8393405113
10 changed files with 260 additions and 73 deletions

View File

@ -13,6 +13,8 @@
#include "numparse_decimal.h"
#include "unicode/numberformatter.h"
#include <typeinfo>
using namespace icu;
using namespace icu::number;
using namespace icu::number::impl;
@ -92,22 +94,121 @@ void NumberParserImpl::freeze() {
fFrozen = true;
}
//void
//NumberParserImpl::parse(const UnicodeString& input, int32_t start, bool greedy, ParsedNumber& result,
// UErrorCode& status) const {
// U_ASSERT(frozen);
// // TODO: Check start >= 0 and start < input.length()
// StringSegment segment(utils::maybeFold(input, parseFlags));
// segment.adjustOffset(start);
// if (greedy) {
// parseGreedyRecursive(segment, result);
// } else {
// parseLongestRecursive(segment, result);
// }
// for (NumberParseMatcher matcher : matchers) {
// matcher.postProcess(result);
// }
//}
void NumberParserImpl::parse(const UnicodeString& input, bool greedy, ParsedNumber& result,
UErrorCode& status) const {
return parse(input, 0, greedy, result, status);
}
void
NumberParserImpl::parse(const UnicodeString& input, int32_t start, bool greedy, ParsedNumber& result,
UErrorCode& status) const {
U_ASSERT(fFrozen);
// TODO: Check start >= 0 and start < input.length()
StringSegment segment(input, fParseFlags);
segment.adjustOffset(start);
if (greedy) {
parseGreedyRecursive(segment, result, status);
} else {
parseLongestRecursive(segment, result, status);
}
for (int32_t i = 0; i < fNumMatchers; i++) {
fMatchers[i]->postProcess(result);
}
}
void NumberParserImpl::parseGreedyRecursive(StringSegment& segment, ParsedNumber& result,
UErrorCode& status) const {
// Base Case
if (segment.length() == 0) {
return;
}
int initialOffset = segment.getOffset();
int leadCp = segment.getCodePoint();
for (int32_t i = 0; i < fNumMatchers; i++) {
if (fComputeLeads && !fLeads[i]->contains(leadCp)) {
continue;
}
const NumberParseMatcher* matcher = fMatchers[i];
matcher->match(segment, result, status);
if (U_FAILURE(status)) {
return;
}
if (segment.getOffset() != initialOffset) {
// In a greedy parse, recurse on only the first match.
parseGreedyRecursive(segment, result, status);
// The following line resets the offset so that the StringSegment says the same across
// the function
// call boundary. Since we recurse only once, this line is not strictly necessary.
segment.setOffset(initialOffset);
return;
}
}
// NOTE: If we get here, the greedy parse completed without consuming the entire string.
}
void NumberParserImpl::parseLongestRecursive(StringSegment& segment, ParsedNumber& result,
UErrorCode& status) const {
// Base Case
if (segment.length() == 0) {
return;
}
// TODO: Give a nice way for the matcher to reset the ParsedNumber?
ParsedNumber initial(result);
ParsedNumber candidate;
int initialOffset = segment.getOffset();
for (int32_t i = 0; i < fNumMatchers; i++) {
// TODO: Check leadChars here?
const NumberParseMatcher* matcher = fMatchers[i];
// In a non-greedy parse, we attempt all possible matches and pick the best.
for (int32_t charsToConsume = 0; charsToConsume < segment.length();) {
charsToConsume += U16_LENGTH(segment.codePointAt(charsToConsume));
// Run the matcher on a segment of the current length.
candidate = initial;
segment.setLength(charsToConsume);
bool maybeMore = matcher->match(segment, candidate, status);
segment.resetLength();
if (U_FAILURE(status)) {
return;
}
// If the entire segment was consumed, recurse.
if (segment.getOffset() - initialOffset == charsToConsume) {
parseLongestRecursive(segment, candidate, status);
if (U_FAILURE(status)) {
return;
}
if (candidate.isBetterThan(result)) {
result = candidate;
}
}
// Since the segment can be re-used, reset the offset.
// This does not have an effect if the matcher did not consume any chars.
segment.setOffset(initialOffset);
// Unless the matcher wants to see the next char, continue to the next matcher.
if (!maybeMore) {
break;
}
}
}
}
UnicodeString NumberParserImpl::toString() const {
UnicodeString result(u"<NumberParserImpl matchers:[");
for (int32_t i = 0; i < fNumMatchers; i++) {
result.append(u' ');
result.append(UnicodeString(typeid(*fMatchers[i]).name()));
}
result.append(u" ]>", -1);
return result;
}
#endif /* #if !UCONFIG_NO_FORMATTING */

View File

@ -42,9 +42,9 @@ class NumberParserImpl {
~NumberParserImpl();
void parseGreedyRecursive(StringSegment& segment, ParsedNumber& result) const;
void parseGreedyRecursive(StringSegment& segment, ParsedNumber& result, UErrorCode& status) const;
void parseLongestRecursive(StringSegment& segment, ParsedNumber& result) const;
void parseLongestRecursive(StringSegment& segment, ParsedNumber& result, UErrorCode& status) const;
};

View File

@ -70,6 +70,11 @@ double ParsedNumber::getDouble() const {
return quantity.toDouble();
}
bool ParsedNumber::isBetterThan(const ParsedNumber& other) {
// Favor results with strictly more characters consumed.
return charEnd > other.charEnd;
}
#endif /* #if !UCONFIG_NO_FORMATTING */

View File

@ -9,13 +9,16 @@
#include "numparse_stringsegment.h"
#include "putilimp.h"
#include "unicode/utf16.h"
#include "unicode/uniset.h"
using namespace icu;
using namespace icu::numparse;
using namespace icu::numparse::impl;
StringSegment::StringSegment(const UnicodeString &str) : fStr(str), fStart(0), fEnd(str.length()) {}
StringSegment::StringSegment(const UnicodeString& str, parse_flags_t parseFlags)
: fStr(str), fStart(0), fEnd(str.length()),
fFoldCase(0 != (parseFlags & PARSE_FLAG_IGNORE_CASE)) {}
int32_t StringSegment::getOffset() const {
return fStart;
@ -29,6 +32,10 @@ void StringSegment::adjustOffset(int32_t delta) {
fStart += delta;
}
void StringSegment::adjustOffsetByCodePoint() {
fStart += U16_LENGTH(getCodePoint());
}
void StringSegment::setLength(int32_t length) {
fEnd = fStart + length;
}
@ -64,10 +71,35 @@ UChar32 StringSegment::getCodePoint() const {
}
}
int32_t StringSegment::getCommonPrefixLength(const UnicodeString &other) {
bool StringSegment::matches(UChar32 otherCp) const {
return codePointsEqual(getCodePoint(), otherCp, fFoldCase);
}
bool StringSegment::matches(const UnicodeSet& uniset) const {
// TODO: Move UnicodeSet case-folding logic here.
// TODO: Handle string matches here instead of separately.
UChar32 cp = getCodePoint();
if (cp == -1) {
return false;
}
return uniset.contains(cp);
}
int32_t StringSegment::getCommonPrefixLength(const UnicodeString& other) {
return getPrefixLengthInternal(other, fFoldCase);
}
int32_t StringSegment::getCaseSensitivePrefixLength(const UnicodeString& other) {
return getPrefixLengthInternal(other, false);
}
int32_t StringSegment::getPrefixLengthInternal(const UnicodeString& other, bool foldCase) {
int32_t offset = 0;
for (; offset < uprv_min(length(), other.length());) {
if (charAt(offset) != other.charAt(offset)) {
// TODO: case-fold code points, not chars
char16_t c1 = charAt(offset);
char16_t c2 = other.charAt(offset);
if (!codePointsEqual(c1, c2, foldCase)) {
break;
}
offset++;
@ -75,5 +107,17 @@ int32_t StringSegment::getCommonPrefixLength(const UnicodeString &other) {
return offset;
}
bool StringSegment::codePointsEqual(UChar32 cp1, UChar32 cp2, bool foldCase) {
if (cp1 == cp2) {
return true;
}
if (!foldCase) {
return false;
}
cp1 = u_foldCase(cp1, TRUE);
cp2 = u_foldCase(cp2, TRUE);
return cp1 == cp2;
}
#endif /* #if !UCONFIG_NO_FORMATTING */

View File

@ -130,6 +130,8 @@ class ParsedNumber {
bool seenNumber() const;
double getDouble() const;
bool isBetterThan(const ParsedNumber& other);
};
@ -141,7 +143,7 @@ class ParsedNumber {
*/
class StringSegment : public UMemory, public ::icu::number::impl::CharSequence {
public:
explicit StringSegment(const UnicodeString& str);
explicit StringSegment(const UnicodeString& str, parse_flags_t parseFlags);
int32_t getOffset() const;
@ -157,6 +159,11 @@ class StringSegment : public UMemory, public ::icu::number::impl::CharSequence {
*/
void adjustOffset(int32_t delta);
/**
* Adjusts the offset by the width of the current code point, either 1 or 2 chars.
*/
void adjustOffsetByCodePoint();
void setLength(int32_t length);
void resetLength();
@ -172,20 +179,51 @@ class StringSegment : public UMemory, public ::icu::number::impl::CharSequence {
/**
* Returns the first code point in the string segment, or -1 if the string starts with an invalid
* code point.
*
* <p>
* <strong>Important:</strong> Most of the time, you should use {@link #matches}, which handles case
* folding logic, instead of this method.
*/
UChar32 getCodePoint() const;
/**
* Returns true if the first code point of this StringSegment equals the given code point.
*
* <p>
* This method will perform case folding if case folding is enabled for the parser.
*/
bool matches(UChar32 otherCp) const;
/**
* Returns true if the first code point of this StringSegment is in the given UnicodeSet.
*/
bool matches(const UnicodeSet& uniset) const;
/**
* Returns the length of the prefix shared by this StringSegment and the given CharSequence. For
* example, if this string segment is "aab", and the char sequence is "aac", this method returns 2,
* since the first 2 characters are the same.
*
* <p>
* This method will perform case folding if case folding is enabled for the parser.
*/
int32_t getCommonPrefixLength(const UnicodeString& other);
/**
* Like {@link #getCommonPrefixLength}, but never performs case folding, even if case folding is
* enabled for the parser.
*/
int32_t getCaseSensitivePrefixLength(const UnicodeString& other);
private:
const UnicodeString fStr;
int32_t fStart;
int32_t fEnd;
bool fFoldCase;
int32_t getPrefixLengthInternal(const UnicodeString& other, bool foldCase);
static bool codePointsEqual(UChar32 cp1, UChar32 cp2, bool foldCase);
};

View File

@ -50,38 +50,39 @@ void NumberParserTest::testBasic() {
{7, u"𝟳𝟴.𝟬𝟬𝟬.𝟬𝟮𝟯", u"#,##,##0", 11, 78.},
{3, u"-𝟱𝟭𝟰𝟮𝟯", u"0", 11, -51423.},
{3, u"-𝟱𝟭𝟰𝟮𝟯-", u"0", 11, -51423.},
{3, u"a51423US dollars", u"a0¤¤¤", 16, 51423.},
{3, u"a 51423 US dollars", u"a0¤¤¤", 18, 51423.},
{3, u"514.23 USD", u"¤0", 10, 514.23},
{3, u"514.23 GBP", u"¤0", 10, 514.23},
{3, u"a 𝟱𝟭𝟰𝟮𝟯 b", u"a0b", 14, 51423.},
{3, u"-a 𝟱𝟭𝟰𝟮𝟯 b", u"a0b", 15, -51423.},
{3, u"a -𝟱𝟭𝟰𝟮𝟯 b", u"a0b", 15, -51423.},
{3, u"𝟱𝟭𝟰𝟮𝟯", u"[0];(0)", 10, 51423.},
{3, u"[𝟱𝟭𝟰𝟮𝟯", u"[0];(0)", 11, 51423.},
{3, u"𝟱𝟭𝟰𝟮𝟯]", u"[0];(0)", 11, 51423.},
{3, u"[𝟱𝟭𝟰𝟮𝟯]", u"[0];(0)", 12, 51423.},
{3, u"(𝟱𝟭𝟰𝟮𝟯", u"[0];(0)", 11, -51423.},
{3, u"𝟱𝟭𝟰𝟮𝟯)", u"[0];(0)", 11, -51423.},
{3, u"(𝟱𝟭𝟰𝟮𝟯)", u"[0];(0)", 12, -51423.},
{3, u"𝟱𝟭𝟰𝟮𝟯", u"{0};{0}", 10, 51423.},
{3, u"{𝟱𝟭𝟰𝟮𝟯", u"{0};{0}", 11, 51423.},
{3, u"𝟱𝟭𝟰𝟮𝟯}", u"{0};{0}", 11, 51423.},
{3, u"{𝟱𝟭𝟰𝟮𝟯}", u"{0};{0}", 12, 51423.},
{1, u"a40b", u"a0'0b'", 3, 40.}, // greedy code path thinks "40" is the number
{2, u"a40b", u"a0'0b'", 4, 4.}, // slow code path finds the suffix "0b"
{3, u"𝟱.𝟭𝟰𝟮E𝟯", u"0", 12, 5142.},
{3, u"𝟱.𝟭𝟰𝟮E-𝟯", u"0", 13, 0.005142},
{3, u"𝟱.𝟭𝟰𝟮e-𝟯", u"0", 13, 0.005142},
{7, u"5,142.50 Canadian dollars", u"#,##,##0 ¤¤¤", 25, 5142.5},
{3, u"a$ b5", u"a ¤ b0", 5, 5.0},
{3, u"📺1.23", u"📺0;📻0", 6, 1.23},
{3, u"📻1.23", u"📺0;📻0", 6, -1.23},
{3, u".00", u"0", 3, 0.0},
{3, u" 0", u"a0", 31, 0.0}, // should not hang
{3, u"NaN", u"0", 3, NAN},
{3, u"NaN E5", u"0", 3, NAN},
{3, u"0", u"0", 1, 0.0}};
// {3, u"a51423US dollars", u"a0¤¤¤", 16, 51423.},
// {3, u"a 51423 US dollars", u"a0¤¤¤", 18, 51423.},
// {3, u"514.23 USD", u"¤0", 10, 514.23},
// {3, u"514.23 GBP", u"¤0", 10, 514.23},
// {3, u"a 𝟱𝟭𝟰𝟮𝟯 b", u"a0b", 14, 51423.},
// {3, u"-a 𝟱𝟭𝟰𝟮𝟯 b", u"a0b", 15, -51423.},
// {3, u"a -𝟱𝟭𝟰𝟮𝟯 b", u"a0b", 15, -51423.},
// {3, u"𝟱𝟭𝟰𝟮𝟯", u"[0];(0)", 10, 51423.},
// {3, u"[𝟱𝟭𝟰𝟮𝟯", u"[0];(0)", 11, 51423.},
// {3, u"𝟱𝟭𝟰𝟮𝟯]", u"[0];(0)", 11, 51423.},
// {3, u"[𝟱𝟭𝟰𝟮𝟯]", u"[0];(0)", 12, 51423.},
// {3, u"(𝟱𝟭𝟰𝟮𝟯", u"[0];(0)", 11, -51423.},
// {3, u"𝟱𝟭𝟰𝟮𝟯)", u"[0];(0)", 11, -51423.},
// {3, u"(𝟱𝟭𝟰𝟮𝟯)", u"[0];(0)", 12, -51423.},
// {3, u"𝟱𝟭𝟰𝟮𝟯", u"{0};{0}", 10, 51423.},
// {3, u"{𝟱𝟭𝟰𝟮𝟯", u"{0};{0}", 11, 51423.},
// {3, u"𝟱𝟭𝟰𝟮𝟯}", u"{0};{0}", 11, 51423.},
// {3, u"{𝟱𝟭𝟰𝟮𝟯}", u"{0};{0}", 12, 51423.},
// {1, u"a40b", u"a0'0b'", 3, 40.}, // greedy code path thinks "40" is the number
// {2, u"a40b", u"a0'0b'", 4, 4.}, // slow code path finds the suffix "0b"
// {3, u"𝟱.𝟭𝟰𝟮E𝟯", u"0", 12, 5142.},
// {3, u"𝟱.𝟭𝟰𝟮E-𝟯", u"0", 13, 0.005142},
// {3, u"𝟱.𝟭𝟰𝟮e-𝟯", u"0", 13, 0.005142},
// {7, u"5,142.50 Canadian dollars", u"#,##,##0 ¤¤¤", 25, 5142.5},
// {3, u"a$ b5", u"a ¤ b0", 5, 5.0},
// {3, u"📺1.23", u"📺0;📻0", 6, 1.23},
// {3, u"📻1.23", u"📺0;📻0", 6, -1.23},
// {3, u".00", u"0", 3, 0.0},
// {3, u" 0", u"a0", 31, 0.0}, // should not hang
// {3, u"NaN", u"0", 3, NAN},
// {3, u"NaN E5", u"0", 3, NAN},
// {3, u"0", u"0", 1, 0.0}
};
parse_flags_t parseFlags = PARSE_FLAG_IGNORE_CASE | PARSE_FLAG_INCLUDE_UNPAIRED_AFFIXES;
for (auto cas : cases) {
@ -123,10 +124,7 @@ void NumberParserTest::testBasic() {
if (0 != (cas.flags & 0x04)) {
// Test with strict separators
parser = NumberParserImpl::createSimpleParser(
Locale("en"),
patternString,
parseFlags | PARSE_FLAG_STRICT_GROUPING_SIZE,
status);
Locale("en"), patternString, parseFlags | PARSE_FLAG_STRICT_GROUPING_SIZE, status);
ParsedNumber resultObject;
parser->parse(inputString, true, resultObject, status);
assertTrue("Strict Parse failed: " + message, resultObject.success());

View File

@ -24,7 +24,7 @@ void StringSegmentTest::runIndexedTest(int32_t index, UBool exec, const char*&na
}
void StringSegmentTest::testOffset() {
StringSegment segment(SAMPLE_STRING);
StringSegment segment(SAMPLE_STRING, 0);
assertEquals("Initial Offset", 0, segment.getOffset());
segment.adjustOffset(3);
assertEquals("Adjust A", 3, segment.getOffset());
@ -35,7 +35,7 @@ void StringSegmentTest::testOffset() {
}
void StringSegmentTest::testLength() {
StringSegment segment(SAMPLE_STRING);
StringSegment segment(SAMPLE_STRING, 0);
assertEquals("Initial length", 11, segment.length());
segment.adjustOffset(3);
assertEquals("Adjust", 8, segment.length());
@ -48,7 +48,7 @@ void StringSegmentTest::testLength() {
}
void StringSegmentTest::testCharAt() {
StringSegment segment(SAMPLE_STRING);
StringSegment segment(SAMPLE_STRING, 0);
assertEquals("Initial", SAMPLE_STRING, segment.toUnicodeString());
segment.adjustOffset(3);
assertEquals("After adjust-offset", UnicodeString(u"radio 📻"), segment.toUnicodeString());
@ -57,7 +57,7 @@ void StringSegmentTest::testCharAt() {
}
void StringSegmentTest::testGetCodePoint() {
StringSegment segment(SAMPLE_STRING);
StringSegment segment(SAMPLE_STRING, 0);
assertEquals("Double-width code point", 0x1F4FB, segment.getCodePoint());
segment.setLength(1);
assertEquals("Inalid A", -1, segment.getCodePoint());
@ -69,7 +69,7 @@ void StringSegmentTest::testGetCodePoint() {
}
void StringSegmentTest::testCommonPrefixLength() {
StringSegment segment(SAMPLE_STRING);
StringSegment segment(SAMPLE_STRING, 0);
assertEquals("", 11, segment.getCommonPrefixLength(SAMPLE_STRING));
assertEquals("", 4, segment.getCommonPrefixLength(u"📻 r"));
assertEquals("", 3, segment.getCommonPrefixLength(u"📻 x"));

View File

@ -5,7 +5,6 @@ package com.ibm.icu.impl.number.parse;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import com.ibm.icu.impl.number.AffixPatternProvider;
@ -268,7 +267,6 @@ public class NumberParserImpl {
private final int parseFlags;
private final List<NumberParseMatcher> matchers;
private final List<UnicodeSet> leads;
private Comparator<ParsedNumber> comparator;
private boolean frozen;
/**
@ -284,7 +282,6 @@ public class NumberParserImpl {
} else {
leads = null;
}
comparator = ParsedNumber.COMPARATOR; // default value
this.parseFlags = parseFlags;
frozen = false;
}
@ -318,11 +315,6 @@ public class NumberParserImpl {
this.leads.add(leadCodePoints);
}
public void setComparator(Comparator<ParsedNumber> comparator) {
assert !frozen;
this.comparator = comparator;
}
public void freeze() {
frozen = true;
}
@ -400,11 +392,12 @@ public class NumberParserImpl {
int initialOffset = segment.getOffset();
for (int i = 0; i < matchers.size(); i++) {
// TODO: Check leadChars here?
NumberParseMatcher matcher = matchers.get(i);
// In a non-greedy parse, we attempt all possible matches and pick the best.
for (int charsToConsume = 0; charsToConsume < segment.length();) {
charsToConsume += Character.charCount(Character.codePointAt(segment, charsToConsume));
charsToConsume += Character.charCount(segment.codePointAt(charsToConsume));
// Run the matcher on a segment of the current length.
candidate.copyFrom(initial);
@ -415,7 +408,7 @@ public class NumberParserImpl {
// If the entire segment was consumed, recurse.
if (segment.getOffset() - initialOffset == charsToConsume) {
parseLongestRecursive(segment, candidate);
if (comparator.compare(candidate, result) > 0) {
if (candidate.isBetterThan(result)) {
result.copyFrom(candidate);
}
}

View File

@ -166,4 +166,8 @@ public class ParsedNumber {
return d;
}
boolean isBetterThan(ParsedNumber other) {
return COMPARATOR.compare(this, other) > 0;
}
}

View File

@ -74,6 +74,10 @@ public class StringSegment implements CharSequence {
return str.charAt(index + start);
}
public int codePointAt(int index) {
return str.codePointAt(index + start);
}
@Override
public CharSequence subSequence(int start, int end) {
throw new AssertionError(); // Never used