mirror of https://github.com/apache/lucene.git
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:
parent
d7d8d5fb45
commit
a28589d2c3
|
@ -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
|
||||
----------------------
|
||||
|
||||
|
|
|
@ -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) &&
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)");
|
||||
|
|
Loading…
Reference in New Issue