Save the original text of parsed numbers

Value concatenation is supposed to provide an illusion that
you can just type long unquoted sentences, but really they
are a series of tokens. Be sure we don't weirdly reformat
any numbers that happen to appear in unquoted text.
This commit is contained in:
Havoc Pennington 2011-11-12 22:42:25 -05:00
parent 52f34925dc
commit 31a8e9ee57
10 changed files with 99 additions and 44 deletions

View File

@ -3,12 +3,12 @@ package com.typesafe.config.impl;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigValueType;
final class ConfigDouble extends AbstractConfigValue {
final class ConfigDouble extends ConfigNumber {
final private double value;
ConfigDouble(ConfigOrigin origin, double value) {
super(origin);
ConfigDouble(ConfigOrigin origin, double value, String originalText) {
super(origin, originalText);
this.value = value;
}
@ -24,6 +24,10 @@ final class ConfigDouble extends AbstractConfigValue {
@Override
String transformToString() {
return Double.toString(value);
String s = super.transformToString();
if (s == null)
return Double.toString(value);
else
return s;
}
}

View File

@ -3,12 +3,12 @@ package com.typesafe.config.impl;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigValueType;
final class ConfigInt extends AbstractConfigValue {
final class ConfigInt extends ConfigNumber {
final private int value;
ConfigInt(ConfigOrigin origin, int value) {
super(origin);
ConfigInt(ConfigOrigin origin, int value, String originalText) {
super(origin, originalText);
this.value = value;
}
@ -48,6 +48,10 @@ final class ConfigInt extends AbstractConfigValue {
@Override
String transformToString() {
return Integer.toString(value);
String s = super.transformToString();
if (s == null)
return Integer.toString(value);
else
return s;
}
}

View File

@ -3,12 +3,12 @@ package com.typesafe.config.impl;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigValueType;
final class ConfigLong extends AbstractConfigValue {
final class ConfigLong extends ConfigNumber {
final private long value;
ConfigLong(ConfigOrigin origin, long value) {
super(origin);
ConfigLong(ConfigOrigin origin, long value, String originalText) {
super(origin, originalText);
this.value = value;
}
@ -53,6 +53,10 @@ final class ConfigLong extends AbstractConfigValue {
@Override
String transformToString() {
return Long.toString(value);
String s = super.transformToString();
if (s == null)
return Long.toString(value);
else
return s;
}
}

View File

@ -0,0 +1,21 @@
package com.typesafe.config.impl;
import com.typesafe.config.ConfigOrigin;
abstract class ConfigNumber extends AbstractConfigValue {
// This is so when we concatenate a number into a string (say it appears in
// a sentence) we always have it exactly as the person typed it into the
// config file. It's purely cosmetic; equals/hashCode don't consider this
// for example.
final private String originalText;
protected ConfigNumber(ConfigOrigin origin, String originalText) {
super(origin);
this.originalText = originalText;
}
@Override
String transformToString() {
return originalText;
}
}

View File

@ -16,13 +16,13 @@ final class DefaultTransformer implements ConfigTransformer {
case NUMBER:
try {
Long v = Long.parseLong(s);
return new ConfigLong(value.origin(), v);
return new ConfigLong(value.origin(), v, s);
} catch (NumberFormatException e) {
// try Double
}
try {
Double v = Double.parseDouble(s);
return new ConfigDouble(value.origin(), v);
return new ConfigDouble(value.origin(), v, s);
} catch (NumberFormatException e) {
// oh well.
}

View File

@ -271,11 +271,11 @@ final class Tokenizer {
try {
if (containedDecimalOrE) {
// force floating point representation
return Tokens
.newDouble(lineOrigin(), Double.parseDouble(s));
return Tokens.newDouble(lineOrigin(),
Double.parseDouble(s), s);
} else {
// this should throw if the integer is too large for Long
return Tokens.newLong(lineOrigin(), Long.parseLong(s));
return Tokens.newLong(lineOrigin(), Long.parseLong(s), s);
}
} catch (NumberFormatException e) {
throw parseError("Invalid number: '" + s

View File

@ -264,16 +264,17 @@ final class Tokens {
return newValue(new ConfigString(origin, value));
}
static Token newInt(ConfigOrigin origin, int value) {
return newValue(new ConfigInt(origin, value));
static Token newInt(ConfigOrigin origin, int value, String originalText) {
return newValue(new ConfigInt(origin, value, originalText));
}
static Token newDouble(ConfigOrigin origin, double value) {
return newValue(new ConfigDouble(origin, value));
static Token newDouble(ConfigOrigin origin, double value,
String originalText) {
return newValue(new ConfigDouble(origin, value, originalText));
}
static Token newLong(ConfigOrigin origin, long value) {
return newValue(new ConfigLong(origin, value));
static Token newLong(ConfigOrigin origin, long value, String originalText) {
return newValue(new ConfigLong(origin, value, originalText));
}
static Token newNull(ConfigOrigin origin) {

View File

@ -14,9 +14,9 @@ class ConfigValueTest extends TestUtils {
@Test
def configIntEquality() {
val a = new ConfigInt(fakeOrigin(), 42)
val sameAsA = new ConfigInt(fakeOrigin(), 42)
val b = new ConfigInt(fakeOrigin(), 43)
val a = intValue(42)
val sameAsA = intValue(42)
val b = intValue(43)
checkEqualObjects(a, a)
checkEqualObjects(a, sameAsA)
@ -25,9 +25,9 @@ class ConfigValueTest extends TestUtils {
@Test
def configLongEquality() {
val a = new ConfigLong(fakeOrigin(), Integer.MAX_VALUE + 42L)
val sameAsA = new ConfigLong(fakeOrigin(), Integer.MAX_VALUE + 42L)
val b = new ConfigLong(fakeOrigin(), Integer.MAX_VALUE + 43L)
val a = longValue(Integer.MAX_VALUE + 42L)
val sameAsA = longValue(Integer.MAX_VALUE + 42L)
val b = longValue(Integer.MAX_VALUE + 43L)
checkEqualObjects(a, a)
checkEqualObjects(a, sameAsA)
@ -36,21 +36,21 @@ class ConfigValueTest extends TestUtils {
@Test
def configIntAndLongEquality() {
val longValue = new ConfigLong(fakeOrigin(), 42L)
val intValue = new ConfigLong(fakeOrigin(), 42)
val longValueB = new ConfigLong(fakeOrigin(), 43L)
val intValueB = new ConfigLong(fakeOrigin(), 43)
val longVal = longValue(42L)
val intValue = longValue(42)
val longValueB = longValue(43L)
val intValueB = longValue(43)
checkEqualObjects(intValue, longValue)
checkEqualObjects(intValue, longVal)
checkEqualObjects(intValueB, longValueB)
checkNotEqualObjects(intValue, longValueB)
checkNotEqualObjects(intValueB, longValue)
checkNotEqualObjects(intValueB, longVal)
}
private def configMap(pairs: (String, Int)*): java.util.Map[String, AbstractConfigValue] = {
val m = new java.util.HashMap[String, AbstractConfigValue]()
for (p <- pairs) {
m.put(p._1, new ConfigInt(fakeOrigin(), p._2))
m.put(p._1, intValue(p._2))
}
m
}
@ -309,4 +309,25 @@ class ConfigValueTest extends TestUtils {
unresolved { dmo.values() }
unresolved { dmo.getInt("foo") }
}
@Test
def roundTripNumbersThroughString() {
// formats rounded off with E notation
val a = "132454454354353245.3254652656454808909932874873298473298472"
// formats as 100000.0
val b = "1e6"
// formats as 5.0E-5
val c = "0.00005"
// formats as 1E100 (capital E)
val d = "1e100"
val obj = parseObject("{ a : " + a + ", b : " + b + ", c : " + c + ", d : " + d + "}")
assertEquals(Seq(a, b, c, d),
Seq("a", "b", "c", "d") map { obj.getString(_) })
// make sure it still works if we're doing concatenation
val obj2 = parseObject("{ a : xx " + a + " yy, b : xx " + b + " yy, c : xx " + c + " yy, d : xx " + d + " yy}")
assertEquals(Seq(a, b, c, d) map { "xx " + _ + " yy" },
Seq("a", "b", "c", "d") map { obj2.getString(_) })
}
}

View File

@ -55,11 +55,11 @@ class JsonTest extends TestUtils {
case lift.JField(name, value) =>
throw new IllegalStateException("either JField was a toplevel from lift-json or this function is buggy")
case lift.JInt(i) =>
if (i.isValidInt) new ConfigInt(fakeOrigin(), i.intValue) else new ConfigLong(fakeOrigin(), i.longValue)
if (i.isValidInt) intValue(i.intValue) else longValue(i.longValue)
case lift.JBool(b) =>
new ConfigBoolean(fakeOrigin(), b)
case lift.JDouble(d) =>
new ConfigDouble(fakeOrigin(), d)
doubleValue(d)
case lift.JString(s) =>
new ConfigString(fakeOrigin(), s)
case lift.JNull =>

View File

@ -311,12 +311,12 @@ abstract trait TestUtils {
}
}
protected def intValue(i: Int) = new ConfigInt(fakeOrigin(), i)
protected def longValue(l: Long) = new ConfigLong(fakeOrigin(), l)
protected def intValue(i: Int) = new ConfigInt(fakeOrigin(), i, null)
protected def longValue(l: Long) = new ConfigLong(fakeOrigin(), l, null)
protected def boolValue(b: Boolean) = new ConfigBoolean(fakeOrigin(), b)
protected def nullValue() = new ConfigNull(fakeOrigin())
protected def stringValue(s: String) = new ConfigString(fakeOrigin(), s)
protected def doubleValue(d: Double) = new ConfigDouble(fakeOrigin(), d)
protected def doubleValue(d: Double) = new ConfigDouble(fakeOrigin(), d, null)
protected def parseObject(s: String) = {
Parser.parse(SyntaxFlavor.CONF, new SimpleConfigOrigin("test string"), s, includer()).asInstanceOf[AbstractConfigObject]
@ -338,9 +338,9 @@ abstract trait TestUtils {
def tokenNull = Tokens.newNull(fakeOrigin())
def tokenUnquoted(s: String) = Tokens.newUnquotedText(fakeOrigin(), s)
def tokenString(s: String) = Tokens.newString(fakeOrigin(), s)
def tokenDouble(d: Double) = Tokens.newDouble(fakeOrigin(), d)
def tokenInt(i: Int) = Tokens.newInt(fakeOrigin(), i)
def tokenLong(l: Long) = Tokens.newLong(fakeOrigin(), l)
def tokenDouble(d: Double) = Tokens.newDouble(fakeOrigin(), d, null)
def tokenInt(i: Int) = Tokens.newInt(fakeOrigin(), i, null)
def tokenLong(l: Long) = Tokens.newLong(fakeOrigin(), l, null)
def tokenSubstitution(expression: Token*) = {
val l = new java.util.ArrayList[Token]