SOLR-4138: CurrencyField fields can now be used in a ValueSources. There is also a new currency(field,[CODE]) function

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1452483 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Chris M. Hostetter 2013-03-04 20:25:37 +00:00
parent d7d8d5fb45
commit a28589d2c3
6 changed files with 331 additions and 8 deletions

View File

@ -98,6 +98,14 @@ New Features
Similarity when you know the optimal "Sweet Spot" of values for the field
length and TF scoring factors. (hossman)
* SOLR-4138: CurrencyField fields can now be used in a ValueSources to
get the "raw" value (using the default number of fractional digits) in
the default currency of the field type. There is also a new
currency(field,[CODE]) function for generating a ValueSource of the
"natural" value, converted to an optionally specified currency to
override the default for the field type.
(hossman)
Bug Fixes
----------------------

View File

@ -242,6 +242,67 @@ public class CurrencyField extends FieldType implements SchemaAware, ResourceLoa
return getRangeQuery(parser, field, valueDefault, valueDefault, true, true);
}
/**
* <p>
* Returns a ValueSource over this field in which the numeric value for
* each document represents the indexed value as converted to the default
* currency for the field, normalized to it's most granular form based
* on the default fractional digits.
* </p>
* <p>
* For example: If the default Currency specified for a field is
* <code>USD</code>, then the values returned by this value source would
* represent the equivilent number of "cents" (ie: value in dollars * 100)
* after converting each document's native currency to USD -- because the
* default fractional digits for <code>USD</code> is "<code>2</code>".
* So for a document whose indexed value was currently equivilent to
* "<code>5.43,USD</code>" using the the exchange provider for this field,
* this ValueSource would return a value of "<code>543<code>"
* </p>
*
* @see #PARAM_DEFAULT_CURRENCY
* @see #DEFAULT_DEFAULT_CURRENCY
* @see Currency#getDefaultFractionDigits
* @see getConvertedValueSource
*/
public RawCurrencyValueSource getValueSource(SchemaField field,
QParser parser) {
field.checkFieldCacheSource(parser);
return new RawCurrencyValueSource(field, defaultCurrency, parser);
}
/**
* <p>
* Returns a ValueSource over this field in which the numeric value for
* each document represents the value from the underlying
* <code>RawCurrencyValueSource</code> as converted to the specified target
* Currency.
* </p>
* <p>
* For example: If the <code>targetCurrencyCode</code> param is set to
* <code>USD</code>, then the values returned by this value source would
* represent the equivilent number of dollars after converting each
* document's raw value to <code>USD</code>. So for a document whose
* indexed value was currently equivilent to "<code>5.43,USD</code>"
* using the the exchange provider for this field, this ValueSource would
* return a value of "<code>5.43<code>"
* </p>
*
* @param targetCurrencyCode The target currency for the resulting value source, if null the defaultCurrency for this field type will be used
* @param source the raw ValueSource to wrap
* @see #PARAM_DEFAULT_CURRENCY
* @see #DEFAULT_DEFAULT_CURRENCY
* @see getValueSource
*/
public ValueSource getConvertedValueSource(String targetCurrencyCode,
RawCurrencyValueSource source) {
if (null == targetCurrencyCode) {
targetCurrencyCode = defaultCurrency;
}
return new ConvertedCurrencyValueSource(targetCurrencyCode,
source);
}
@Override
public Query getRangeQuery(QParser parser, SchemaField field, String part1, String part2, final boolean minInclusive, final boolean maxInclusive) {
final CurrencyValue p1 = CurrencyValue.parse(part1, defaultCurrency);
@ -263,7 +324,7 @@ public class CurrencyField extends FieldType implements SchemaAware, ResourceLoa
// ValueSourceRangeFilter doesn't check exists(), so we have to
final Filter docsWithValues = new FieldValueFilter(getAmountField(field).getName());
final Filter vsRangeFilter = new ValueSourceRangeFilter
(new CurrencyValueSource(field, currencyCode, parser),
(new RawCurrencyValueSource(field, currencyCode, parser),
p1 == null ? null : p1.getAmount() + "",
p2 == null ? null : p2.getAmount() + "",
minInclusive, maxInclusive);
@ -277,7 +338,7 @@ public class CurrencyField extends FieldType implements SchemaAware, ResourceLoa
@Override
public SortField getSortField(SchemaField field, boolean reverse) {
// Convert all values to default currency for sorting.
return (new CurrencyValueSource(field, defaultCurrency, null)).getSortField(reverse);
return (new RawCurrencyValueSource(field, defaultCurrency, null)).getSortField(reverse);
}
@Override
@ -289,14 +350,128 @@ public class CurrencyField extends FieldType implements SchemaAware, ResourceLoa
return provider;
}
class CurrencyValueSource extends ValueSource {
/**
* <p>
* A value source whose values represent the "normal" values
* in the specified target currency.
* </p>
* @see RawCurrencyValueSource
*/
class ConvertedCurrencyValueSource extends ValueSource {
private final Currency targetCurrency;
private final RawCurrencyValueSource source;
private final double rate;
public ConvertedCurrencyValueSource(String targetCurrencyCode,
RawCurrencyValueSource source) {
this.source = source;
this.targetCurrency = getCurrency(targetCurrencyCode);
if (null == targetCurrency) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Currency code not supported by this JVM: " + targetCurrencyCode);
}
// the target digits & currency of our source,
// become the source digits & currency of ourselves
this.rate = provider.getExchangeRate
(source.getTargetCurrency().getCurrencyCode(),
targetCurrency.getCurrencyCode());
}
@Override
public FunctionValues getValues(Map context, AtomicReaderContext reader)
throws IOException {
final FunctionValues amounts = source.getValues(context, reader);
// the target digits & currency of our source,
// become the source digits & currency of ourselves
final String sourceCurrencyCode = source.getTargetCurrency().getCurrencyCode();
final int sourceFractionDigits = source.getTargetCurrency().getDefaultFractionDigits();
final double divisor = Math.pow(10D, targetCurrency.getDefaultFractionDigits());
return new FunctionValues() {
@Override
public boolean exists(int doc) {
return amounts.exists(doc);
}
@Override
public long longVal(int doc) {
return (long) doubleVal(doc);
}
@Override
public int intVal(int doc) {
return (int) doubleVal(doc);
}
@Override
public double doubleVal(int doc) {
return CurrencyValue.convertAmount(rate, sourceCurrencyCode, amounts.longVal(doc), targetCurrency.getCurrencyCode()) / divisor;
}
@Override
public float floatVal(int doc) {
return CurrencyValue.convertAmount(rate, sourceCurrencyCode, amounts.longVal(doc), targetCurrency.getCurrencyCode()) / ((float)divisor);
}
@Override
public String strVal(int doc) {
return Double.toString(doubleVal(doc));
}
@Override
public String toString(int doc) {
return name() + '(' + strVal(doc) + ')';
}
};
}
public String name() {
return "currency";
}
@Override
public String description() {
return name() + "(" + source.getField().getName() + "," + targetCurrency.getCurrencyCode()+")";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ConvertedCurrencyValueSource that = (ConvertedCurrencyValueSource) o;
return !(source != null ? !source.equals(that.source) : that.source != null) &&
(rate == that.rate) &&
!(targetCurrency != null ? !targetCurrency.equals(that.targetCurrency) : that.targetCurrency != null);
}
@Override
public int hashCode() {
int result = targetCurrency != null ? targetCurrency.hashCode() : 0;
result = 31 * result + (source != null ? source.hashCode() : 0);
result = 31 * (int) Double.doubleToLongBits(rate);
return result;
}
}
/**
* <p>
* A value source whose values represent the "raw" (ie: normalized using
* the number of default fractional digits) values in the specified
* target currency).
* </p>
* <p>
* For example: if the specified target currency is "<code>USD</code>"
* then the numeric values are the number of pennies in the value
* (ie: <code>$n * 100</code>) since the number of defalt fractional
* digits for <code>USD</code> is "<code>2</code>")
* </p>
* @see ConvertedCurrencValueSource
*/
class RawCurrencyValueSource extends ValueSource {
private static final long serialVersionUID = 1L;
private Currency targetCurrency;
private final Currency targetCurrency;
private ValueSource currencyValues;
private ValueSource amountValues;
private final SchemaField sf;
public CurrencyValueSource(SchemaField sfield, String targetCurrencyCode, QParser parser) {
public RawCurrencyValueSource(SchemaField sfield, String targetCurrencyCode, QParser parser) {
this.sf = sfield;
this.targetCurrency = getCurrency(targetCurrencyCode);
if (null == targetCurrency) {
@ -309,6 +484,9 @@ public class CurrencyField extends FieldType implements SchemaAware, ResourceLoa
currencyValues = currencyField.getType().getValueSource(currencyField, parser);
amountValues = amountField.getType().getValueSource(amountField, parser);
}
public SchemaField getField() { return sf; }
public Currency getTargetCurrency() { return targetCurrency; }
@Override
public FunctionValues getValues(Map context, AtomicReaderContext reader) throws IOException {
@ -444,12 +622,13 @@ public class CurrencyField extends FieldType implements SchemaAware, ResourceLoa
}
public String name() {
return "currency";
return "rawcurrency";
}
@Override
public String description() {
return name() + "(" + sf.getName() + ")";
return name() + "(" + sf.getName() +
",target="+targetCurrency.getCurrencyCode()+")";
}
@Override
@ -457,7 +636,7 @@ public class CurrencyField extends FieldType implements SchemaAware, ResourceLoa
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CurrencyValueSource that = (CurrencyValueSource) o;
RawCurrencyValueSource that = (RawCurrencyValueSource) o;
return !(amountValues != null ? !amountValues.equals(that.amountValues) : that.amountValues != null) &&
!(currencyValues != null ? !currencyValues.equals(that.currencyValues) : that.currencyValues != null) &&

View File

@ -392,6 +392,21 @@ public abstract class ValueSourceParser implements NamedListInitializedPlugin {
return f.getType().getValueSource(f, fp);
}
});
addParser("currency", new ValueSourceParser() {
@Override
public ValueSource parse(FunctionQParser fp) throws SyntaxError {
String fieldName = fp.parseArg();
SchemaField f = fp.getReq().getSchema().getField(fieldName);
if (! (f.getType() instanceof CurrencyField)) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
"Currency function input must be the name of a CurrencyField: " + fieldName);
}
CurrencyField ft = (CurrencyField) f.getType();
String code = fp.hasMoreArguments() ? fp.parseArg() : null;
return ft.getConvertedValueSource(code, ft.getValueSource(f, fp));
}
});
addParser(new DoubleParser("rad") {
@Override

View File

@ -45,6 +45,7 @@
<fieldType name="tfloat" class="solr.TrieFloatField" precisionStep="8" positionIncrementGap="0"/>
<fieldType name="tlong" class="solr.TrieLongField" precisionStep="8" positionIncrementGap="0"/>
<fieldType name="tdouble" class="solr.TrieDoubleField" precisionStep="8" positionIncrementGap="0"/>
<fieldType name="currency" class="solr.CurrencyField" currencyConfig="currency.xml" multiValued="false" />
<!-- numeric field types that manipulate the value into
a string value that isn't human readable in it's internal form,
@ -518,6 +519,7 @@
<field name="pointD" type="xyd" indexed="true" stored="true" multiValued="false"/>
<field name="point_hash" type="geohash" indexed="true" stored="true" multiValued="false"/>
<field name="store" type="location" indexed="true" stored="true"/>
<field name="amount" type="currency" indexed="true" stored="true" multiValued="false"/>
<!-- to test uniq fields -->
<field name="uniq" type="string" indexed="true" stored="true" multiValued="true"/>

View File

@ -297,6 +297,118 @@ public abstract class AbstractCurrencyFieldTest extends SolrTestCaseJ4 {
assertQ(req("fl", "*,score", "q", "*:*", "sort", field()+" asc", "limit", "1"), "//int[@name='id']='3'");
}
public void testFunctionUsage() throws Exception {
clearIndex();
for (int i = 1; i <= 8; i++) {
// "GBP" currency code is 1/2 of a USD dollar, for testing.
assertU(adoc("id", "" + i, field(), (((float)i)/2) + ",GBP"));
}
for (int i = 9; i <= 11; i++) {
assertU(adoc("id", "" + i, field(), i + ",USD"));
}
assertU(commit());
// direct value source usage, gets "raw" form od default curency
// default==USD, so raw==penies
assertQ(req("fl", "id,func:field($f)",
"f", field(),
"q", "id:5"),
"//*[@numFound='1']",
"//doc/float[@name='func' and .=500]");
assertQ(req("fl", "id,func:field($f)",
"f", field(),
"q", "id:10"),
"//*[@numFound='1']",
"//doc/float[@name='func' and .=1000]");
assertQ(req("fl", "id,score,"+field(),
"q", "{!frange u=500}"+field())
,"//*[@numFound='5']"
,"//int[@name='id']='1'"
,"//int[@name='id']='2'"
,"//int[@name='id']='3'"
,"//int[@name='id']='4'"
,"//int[@name='id']='5'"
);
assertQ(req("fl", "id,score,"+field(),
"q", "{!frange l=500 u=1000}"+field())
,"//*[@numFound='6']"
,"//int[@name='id']='5'"
,"//int[@name='id']='6'"
,"//int[@name='id']='7'"
,"//int[@name='id']='8'"
,"//int[@name='id']='9'"
,"//int[@name='id']='10'"
);
// use the currency function to convert to default (USD)
assertQ(req("fl", "id,func:currency($f)",
"f", field(),
"q", "id:10"),
"//*[@numFound='1']",
"//doc/float[@name='func' and .=10]");
assertQ(req("fl", "id,func:currency($f)",
"f", field(),
"q", "id:5"),
"//*[@numFound='1']",
"//doc/float[@name='func' and .=5]");
assertQ(req("fl", "id,score"+field(),
"f", field(),
"q", "{!frange u=5}currency($f)")
,"//*[@numFound='5']"
,"//int[@name='id']='1'"
,"//int[@name='id']='2'"
,"//int[@name='id']='3'"
,"//int[@name='id']='4'"
,"//int[@name='id']='5'"
);
assertQ(req("fl", "id,score"+field(),
"f", field(),
"q", "{!frange l=5 u=10}currency($f)")
,"//*[@numFound='6']"
,"//int[@name='id']='5'"
,"//int[@name='id']='6'"
,"//int[@name='id']='7'"
,"//int[@name='id']='8'"
,"//int[@name='id']='9'"
,"//int[@name='id']='10'"
);
// use the currency function to convert to MXN
assertQ(req("fl", "id,func:currency($f,MXN)",
"f", field(),
"q", "id:5"),
"//*[@numFound='1']",
"//doc/float[@name='func' and .=10]");
assertQ(req("fl", "id,func:currency($f,MXN)",
"f", field(),
"q", "id:10"),
"//*[@numFound='1']",
"//doc/float[@name='func' and .=20]");
assertQ(req("fl", "*,score,"+field(),
"f", field(),
"q", "{!frange u=10}currency($f,MXN)")
,"//*[@numFound='5']"
,"//int[@name='id']='1'"
,"//int[@name='id']='2'"
,"//int[@name='id']='3'"
,"//int[@name='id']='4'"
,"//int[@name='id']='5'"
);
assertQ(req("fl", "*,score,"+field(),
"f", field(),
"q", "{!frange l=10 u=20}currency($f,MXN)")
,"//*[@numFound='6']"
,"//int[@name='id']='5'"
,"//int[@name='id']='6'"
,"//int[@name='id']='7'"
,"//int[@name='id']='8'"
,"//int[@name='id']='9'"
,"//int[@name='id']='10'"
);
}
@Test
public void testMockFieldType() throws Exception {
clearIndex();

View File

@ -679,6 +679,13 @@ public class QueryEqualityTest extends SolrTestCaseJ4 {
"field('foo_i\')",
"foo_i");
}
public void testFuncCurrency() throws Exception {
assertFuncEquals("currency(\"amount\")",
"currency('amount\')",
"currency(amount)",
"currency(amount,USD)",
"currency('amount',USD)");
}
public void testTestFuncs() throws Exception {
assertFuncEquals("sleep(1,5)", "sleep(1,5)");