You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ab...@apache.org on 2018/01/23 13:27:46 UTC

[14/51] lucene-solr:jira/solr-11714: SOLR-3218: Added range faceting support for CurrencyFieldType

SOLR-3218: Added range faceting support for CurrencyFieldType


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/6dcbb2d4
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/6dcbb2d4
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/6dcbb2d4

Branch: refs/heads/jira/solr-11714
Commit: 6dcbb2d412028075b84567edd60d7fcb56032d14
Parents: 972df6c
Author: Chris Hostetter <ho...@apache.org>
Authored: Sun Jan 14 16:30:24 2018 -0700
Committer: Chris Hostetter <ho...@apache.org>
Committed: Sun Jan 14 16:30:24 2018 -0700

----------------------------------------------------------------------
 solr/CHANGES.txt                                |   3 +
 .../handler/component/RangeFacetRequest.java    |  73 ++-
 .../apache/solr/schema/CurrencyFieldType.java   | 165 +------
 .../org/apache/solr/schema/CurrencyValue.java   | 231 +++++++++
 .../apache/solr/search/facet/FacetRange.java    | 115 ++++-
 .../solr/schema/CurrencyFieldTypeTest.java      | 235 +++++++++
 .../search/CurrencyRangeFacetCloudTest.java     | 483 +++++++++++++++++++
 .../conf/velocity/VM_global_library.vm          |   6 +-
 ...king-with-currencies-and-exchange-rates.adoc |   1 +
 .../client/solrj/response/QueryResponse.java    |  14 +-
 .../solr/client/solrj/response/RangeFacet.java  |   6 +
 11 files changed, 1162 insertions(+), 170 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/6dcbb2d4/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index b14d79e..3d1cda2 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -84,6 +84,9 @@ New Features
 
 * SOLR-11063: Suggesters should accept required freedisk as a hint (noble)
 
+* SOLR-3218: Added range faceting support for CurrencyFieldType.  This includes both "facet.range" as well
+  as json.facet's "type:range" (Andrew Morrison, Jan Høydahl, Vitaliy Zhovtyuk, hossman)
+
 Bug Fixes
 ----------------------
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/6dcbb2d4/solr/core/src/java/org/apache/solr/handler/component/RangeFacetRequest.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/component/RangeFacetRequest.java b/solr/core/src/java/org/apache/solr/handler/component/RangeFacetRequest.java
index c234866..8d47a93 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/RangeFacetRequest.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/RangeFacetRequest.java
@@ -31,8 +31,11 @@ import org.apache.solr.common.params.RequiredSolrParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.schema.CurrencyFieldType;
+import org.apache.solr.schema.CurrencyValue;
 import org.apache.solr.schema.DatePointField;
 import org.apache.solr.schema.DateRangeField;
+import org.apache.solr.schema.ExchangeRateProvider;
 import org.apache.solr.schema.FieldType;
 import org.apache.solr.schema.IndexSchema;
 import org.apache.solr.schema.SchemaField;
@@ -189,6 +192,8 @@ public class RangeFacetRequest extends FacetComponent.FacetBase {
               (SolrException.ErrorCode.BAD_REQUEST,
                   "Unable to range facet on Point field of unexpected type:" + this.facetOn);
       }
+    } else if (ft instanceof CurrencyFieldType) {
+      calc = new CurrencyRangeEndpointCalculator(this);
     } else {
       throw new SolrException
           (SolrException.ErrorCode.BAD_REQUEST,
@@ -451,12 +456,14 @@ public class RangeFacetRequest extends FacetComponent.FacetBase {
       this.field = rfr.getSchemaField();
     }
 
-    public T getComputedEnd() {
+    /** The Computed End point of all ranges, as an Object of type suitable for direct inclusion in the response data */
+    public Object getComputedEnd() {
       assert computed;
       return computedEnd;
     }
 
-    public T getStart() {
+    /** The Start point of all ranges, as an Object of type suitable for direct inclusion in the response data */
+    public Object getStart() {
       assert computed;
       return start;
     }
@@ -756,6 +763,68 @@ public class RangeFacetRequest extends FacetComponent.FacetBase {
     }
   }
 
+  private static class CurrencyRangeEndpointCalculator
+    extends RangeEndpointCalculator<CurrencyValue> {
+    private String defaultCurrencyCode;
+    private ExchangeRateProvider exchangeRateProvider;
+    public CurrencyRangeEndpointCalculator(final RangeFacetRequest rangeFacetRequest) {
+      super(rangeFacetRequest);
+      if(!(this.field.getType() instanceof CurrencyFieldType)) {
+        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
+                                "Cannot perform range faceting over non CurrencyField fields");
+      }
+      defaultCurrencyCode =
+        ((CurrencyFieldType)this.field.getType()).getDefaultCurrency();
+      exchangeRateProvider =
+        ((CurrencyFieldType)this.field.getType()).getProvider();
+    }
+
+    @Override
+    protected Object parseGap(String rawval) throws java.text.ParseException {
+      return parseVal(rawval).strValue();
+    }
+
+    @Override
+    public String formatValue(CurrencyValue val) {
+      return val.strValue();
+    }
+
+    /** formats the value as a String since {@link CurrencyValue} is not suitable for response writers */
+    @Override
+    public Object getComputedEnd() {
+      assert computed;
+      return formatValue(computedEnd);
+    }
+    
+    /** formats the value as a String since {@link CurrencyValue} is not suitable for response writers */
+    @Override
+    public Object getStart() {
+      assert computed;
+      return formatValue(start);
+    }
+    
+    @Override
+    protected CurrencyValue parseVal(String rawval) {
+      return CurrencyValue.parse(rawval, defaultCurrencyCode);
+    }
+
+    @Override
+    public CurrencyValue parseAndAddGap(CurrencyValue value, String gap) {
+      if(value == null) {
+        throw new NullPointerException("Cannot perform range faceting on null CurrencyValue");
+      }
+      CurrencyValue gapCurrencyValue =
+        CurrencyValue.parse(gap, defaultCurrencyCode);
+      long gapAmount =
+        CurrencyValue.convertAmount(this.exchangeRateProvider,
+                                    gapCurrencyValue.getCurrencyCode(),
+                                    gapCurrencyValue.getAmount(),
+                                    value.getCurrencyCode());
+      return new CurrencyValue(value.getAmount() + gapAmount,
+                               value.getCurrencyCode());
+    }
+  }
+  
   /**
    * Represents a single facet range (or gap) for which the count is to be calculated
    */

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/6dcbb2d4/solr/core/src/java/org/apache/solr/schema/CurrencyFieldType.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/schema/CurrencyFieldType.java b/solr/core/src/java/org/apache/solr/schema/CurrencyFieldType.java
index a6ba164..97195da 100644
--- a/solr/core/src/java/org/apache/solr/schema/CurrencyFieldType.java
+++ b/solr/core/src/java/org/apache/solr/schema/CurrencyFieldType.java
@@ -89,6 +89,12 @@ public class CurrencyFieldType extends FieldType implements SchemaAware, Resourc
     return null;
   }
 
+  /** The identifier code for the default currency of this field type */
+  public String getDefaultCurrency() {
+    return defaultCurrency;
+  }
+
+
   @Override
   protected void init(IndexSchema schema, Map<String, String> args) {
     super.init(schema, args);
@@ -666,164 +672,5 @@ public class CurrencyFieldType extends FieldType implements SchemaAware, Resourc
     }
   }
 
-  /**
-   * Represents a Currency field value, which includes a long amount and ISO currency code.
-   */
-  static class CurrencyValue {
-    private long amount;
-    private String currencyCode;
-  
-    /**
-     * Constructs a new currency value.
-     *
-     * @param amount       The amount.
-     * @param currencyCode The currency code.
-     */
-    public CurrencyValue(long amount, String currencyCode) {
-      this.amount = amount;
-      this.currencyCode = currencyCode;
-    }
-  
-    /**
-     * Constructs a new currency value by parsing the specific input.
-     * <p/>
-     * Currency values are expected to be in the format &lt;amount&gt;,&lt;currency code&gt;,
-     * for example, "500,USD" would represent 5 U.S. Dollars.
-     * <p/>
-     * If no currency code is specified, the default is assumed.
-     *
-     * @param externalVal The value to parse.
-     * @param defaultCurrency The default currency.
-     * @return The parsed CurrencyValue.
-     */
-    public static CurrencyValue parse(String externalVal, String defaultCurrency) {
-      if (externalVal == null) {
-        return null;
-      }
-      String amount = externalVal;
-      String code = defaultCurrency;
-  
-      if (externalVal.contains(",")) {
-        String[] amountAndCode = externalVal.split(",");
-        amount = amountAndCode[0];
-        code = amountAndCode[1];
-      }
-  
-      if (amount.equals("*")) {
-        return null;
-      }
-      
-      Currency currency = getCurrency(code);
-  
-      if (currency == null) {
-        throw new SolrException(ErrorCode.BAD_REQUEST, "Currency code not supported by this JVM: " + code);
-      }
-  
-      try {
-        double value = Double.parseDouble(amount);
-        long currencyValue = Math.round(value * Math.pow(10.0, currency.getDefaultFractionDigits()));
-  
-        return new CurrencyValue(currencyValue, code);
-      } catch (NumberFormatException e) {
-        throw new SolrException(ErrorCode.BAD_REQUEST, e);
-      }
-    }
-  
-    /**
-     * The amount of the CurrencyValue.
-     *
-     * @return The amount.
-     */
-    public long getAmount() {
-      return amount;
-    }
-  
-    /**
-     * The ISO currency code of the CurrencyValue.
-     *
-     * @return The currency code.
-     */
-    public String getCurrencyCode() {
-      return currencyCode;
-    }
-  
-    /**
-     * Performs a currency conversion & unit conversion.
-     *
-     * @param exchangeRates      Exchange rates to apply.
-     * @param sourceCurrencyCode The source currency code.
-     * @param sourceAmount       The source amount.
-     * @param targetCurrencyCode The target currency code.
-     * @return The converted indexable units after the exchange rate and currency fraction digits are applied.
-     */
-    public static long convertAmount(ExchangeRateProvider exchangeRates, String sourceCurrencyCode, long sourceAmount, String targetCurrencyCode) {
-      double exchangeRate = exchangeRates.getExchangeRate(sourceCurrencyCode, targetCurrencyCode);
-      return convertAmount(exchangeRate, sourceCurrencyCode, sourceAmount, targetCurrencyCode);
-    }
-  
-    /**
-     * Performs a currency conversion & unit conversion.
-     *
-     * @param exchangeRate         Exchange rate to apply.
-     * @param sourceFractionDigits The fraction digits of the source.
-     * @param sourceAmount         The source amount.
-     * @param targetFractionDigits The fraction digits of the target.
-     * @return The converted indexable units after the exchange rate and currency fraction digits are applied.
-     */
-    public static long convertAmount(final double exchangeRate, final int sourceFractionDigits, final long sourceAmount, final int targetFractionDigits) {
-      int digitDelta = targetFractionDigits - sourceFractionDigits;
-      double value = ((double) sourceAmount * exchangeRate);
-  
-      if (digitDelta != 0) {
-        if (digitDelta < 0) {
-          for (int i = 0; i < -digitDelta; i++) {
-            value *= 0.1;
-          }
-        } else {
-          for (int i = 0; i < digitDelta; i++) {
-            value *= 10.0;
-          }
-        }
-      }
-  
-      return (long) value;
-    }
-  
-    /**
-     * Performs a currency conversion & unit conversion.
-     *
-     * @param exchangeRate       Exchange rate to apply.
-     * @param sourceCurrencyCode The source currency code.
-     * @param sourceAmount       The source amount.
-     * @param targetCurrencyCode The target currency code.
-     * @return The converted indexable units after the exchange rate and currency fraction digits are applied.
-     */
-    public static long convertAmount(double exchangeRate, String sourceCurrencyCode, long sourceAmount, String targetCurrencyCode) {
-      if (targetCurrencyCode.equals(sourceCurrencyCode)) {
-        return sourceAmount;
-      }
-  
-      int sourceFractionDigits = Currency.getInstance(sourceCurrencyCode).getDefaultFractionDigits();
-      Currency targetCurrency = Currency.getInstance(targetCurrencyCode);
-      int targetFractionDigits = targetCurrency.getDefaultFractionDigits();
-      return convertAmount(exchangeRate, sourceFractionDigits, sourceAmount, targetFractionDigits);
-    }
-  
-    /**
-     * Returns a new CurrencyValue that is the conversion of this CurrencyValue to the specified currency.
-     *
-     * @param exchangeRates      The exchange rate provider.
-     * @param targetCurrencyCode The target currency code to convert this CurrencyValue to.
-     * @return The converted CurrencyValue.
-     */
-    public CurrencyValue convertTo(ExchangeRateProvider exchangeRates, String targetCurrencyCode) {
-      return new CurrencyValue(convertAmount(exchangeRates, this.getCurrencyCode(), this.getAmount(), targetCurrencyCode), targetCurrencyCode);
-    }
-  
-    @Override
-    public String toString() {
-      return String.valueOf(amount) + "," + currencyCode;
-    }
-  }
 }
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/6dcbb2d4/solr/core/src/java/org/apache/solr/schema/CurrencyValue.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/schema/CurrencyValue.java b/solr/core/src/java/org/apache/solr/schema/CurrencyValue.java
new file mode 100644
index 0000000..4c43422
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/schema/CurrencyValue.java
@@ -0,0 +1,231 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.schema;
+
+import org.apache.solr.common.SolrException;
+
+import java.util.Currency;
+
+/**
+ * Represents a Currency field value, which includes a long amount and ISO currency code.
+ */
+public class CurrencyValue implements Comparable<CurrencyValue> {
+  private long amount;
+  private String currencyCode;
+
+  /**
+   * Constructs a new currency value.
+   *
+   * @param amount       The amount.
+   * @param currencyCode The currency code.
+   */
+  public CurrencyValue(long amount, String currencyCode) {
+    this.amount = amount;
+    this.currencyCode = currencyCode;
+  }
+
+  /**
+   * Constructs a new currency value by parsing the specific input.
+   * <p>
+   * Currency values are expected to be in the format &lt;amount&gt;,&lt;currency code&gt;,
+   * for example, "500,USD" would represent 5 U.S. Dollars.
+   * </p>
+   * <p>
+   * If no currency code is specified, the default is assumed.
+   * </p>
+   * @param externalVal The value to parse.
+   * @param defaultCurrency The default currency.
+   * @return The parsed CurrencyValue.
+   */
+  public static CurrencyValue parse(String externalVal, String defaultCurrency) {
+    if (externalVal == null) {
+      return null;
+    }
+    String amount = externalVal;
+    String code = defaultCurrency;
+
+    if (externalVal.contains(",")) {
+      String[] amountAndCode = externalVal.split(",");
+      amount = amountAndCode[0];
+      code = amountAndCode[1];
+    }
+
+    if (amount.equals("*")) {
+      return null;
+    }
+
+    Currency currency = CurrencyField.getCurrency(code);
+
+    if (currency == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Currency code not supported by this JVM: " + code);
+    }
+
+    try {
+      double value = Double.parseDouble(amount);
+      long currencyValue = Math.round(value * Math.pow(10.0, currency.getDefaultFractionDigits()));
+
+      return new CurrencyValue(currencyValue, code);
+    } catch (NumberFormatException e) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
+    }
+  }
+
+  /**
+   * The amount of the CurrencyValue.
+   *
+   * @return The amount.
+   */
+  public long getAmount() {
+    return amount;
+  }
+
+  /**
+   * The ISO currency code of the CurrencyValue.
+   *
+   * @return The currency code.
+   */
+  public String getCurrencyCode() {
+    return currencyCode;
+  }
+
+  /**
+   * Performs a currency conversion &amp; unit conversion.
+   *
+   * @param exchangeRates      Exchange rates to apply.
+   * @param sourceCurrencyCode The source currency code.
+   * @param sourceAmount       The source amount.
+   * @param targetCurrencyCode The target currency code.
+   * @return The converted indexable units after the exchange rate and currency fraction digits are applied.
+   */
+  public static long convertAmount(ExchangeRateProvider exchangeRates, String sourceCurrencyCode, long sourceAmount, String targetCurrencyCode) {
+    double exchangeRate = exchangeRates.getExchangeRate(sourceCurrencyCode, targetCurrencyCode);
+    return convertAmount(exchangeRate, sourceCurrencyCode, sourceAmount, targetCurrencyCode);
+  }
+
+  /**
+   * Performs a currency conversion &amp; unit conversion.
+   *
+   * @param exchangeRate         Exchange rate to apply.
+   * @param sourceFractionDigits The fraction digits of the source.
+   * @param sourceAmount         The source amount.
+   * @param targetFractionDigits The fraction digits of the target.
+   * @return The converted indexable units after the exchange rate and currency fraction digits are applied.
+   */
+  public static long convertAmount(final double exchangeRate, final int sourceFractionDigits, final long sourceAmount, final int targetFractionDigits) {
+    int digitDelta = targetFractionDigits - sourceFractionDigits;
+    double value = ((double) sourceAmount * exchangeRate);
+
+    if (digitDelta != 0) {
+      if (digitDelta < 0) {
+        for (int i = 0; i < -digitDelta; i++) {
+          value *= 0.1;
+        }
+      } else {
+        for (int i = 0; i < digitDelta; i++) {
+          value *= 10.0;
+        }
+      }
+    }
+
+    return (long) value;
+  }
+
+  /**
+   * Performs a currency conversion &amp; unit conversion.
+   *
+   * @param exchangeRate       Exchange rate to apply.
+   * @param sourceCurrencyCode The source currency code.
+   * @param sourceAmount       The source amount.
+   * @param targetCurrencyCode The target currency code.
+   * @return The converted indexable units after the exchange rate and currency fraction digits are applied.
+   */
+  public static long convertAmount(double exchangeRate, String sourceCurrencyCode, long sourceAmount, String targetCurrencyCode) {
+    if (targetCurrencyCode.equals(sourceCurrencyCode)) {
+      return sourceAmount;
+    }
+
+    int sourceFractionDigits = Currency.getInstance(sourceCurrencyCode).getDefaultFractionDigits();
+    Currency targetCurrency = Currency.getInstance(targetCurrencyCode);
+    int targetFractionDigits = targetCurrency.getDefaultFractionDigits();
+    return convertAmount(exchangeRate, sourceFractionDigits, sourceAmount, targetFractionDigits);
+  }
+
+  /**
+   * Returns a new CurrencyValue that is the conversion of this CurrencyValue to the specified currency.
+   *
+   * @param exchangeRates      The exchange rate provider.
+   * @param targetCurrencyCode The target currency code to convert this CurrencyValue to.
+   * @return The converted CurrencyValue.
+   */
+  public CurrencyValue convertTo(ExchangeRateProvider exchangeRates, String targetCurrencyCode) {
+    return new CurrencyValue(convertAmount(exchangeRates, this.getCurrencyCode(), this.getAmount(), targetCurrencyCode), targetCurrencyCode);
+  }
+
+  /**
+   * Returns a string representing the currency value such as "3.14,USD" for
+   * a CurrencyValue of $3.14 USD.
+   */
+  public String strValue() {
+    int digits = 0;
+    try {
+      Currency currency =
+        Currency.getInstance(this.getCurrencyCode());
+      if (currency == null) {
+        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
+            "Invalid currency code " + this.getCurrencyCode());
+  }
+      digits = currency.getDefaultFractionDigits();
+}
+    catch(IllegalArgumentException exception) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
+          "Invalid currency code " + this.getCurrencyCode());
+    }
+
+    String amount = Long.toString(this.getAmount());
+    if (this.getAmount() == 0) {
+      amount += "000000".substring(0,digits);
+    }
+    return
+      amount.substring(0, amount.length() - digits)
+      + "." + amount.substring(amount.length() - digits)
+      + "," +  this.getCurrencyCode();
+  }
+
+  @Override
+  public int compareTo(CurrencyValue o) {
+    if(o == null) {
+      throw new NullPointerException("Cannot compare CurrencyValue to a null values");
+    }
+    if(!getCurrencyCode().equals(o.getCurrencyCode())) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
+          "Cannot compare CurrencyValues when their currencies are not equal");
+    }
+    if(o.getAmount() < getAmount()) {
+      return 1;
+    }
+    if(o.getAmount() == getAmount()) {
+      return 0;
+    }
+    return -1;
+  }
+
+  @Override
+  public String toString() {
+    return strValue();
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/6dcbb2d4/solr/core/src/java/org/apache/solr/search/facet/FacetRange.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/search/facet/FacetRange.java b/solr/core/src/java/org/apache/solr/search/facet/FacetRange.java
index 1176a77..09b8ec0 100644
--- a/solr/core/src/java/org/apache/solr/search/facet/FacetRange.java
+++ b/solr/core/src/java/org/apache/solr/search/facet/FacetRange.java
@@ -29,6 +29,9 @@ import org.apache.lucene.util.NumericUtils;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.FacetParams;
 import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.schema.CurrencyFieldType;
+import org.apache.solr.schema.CurrencyValue;
+import org.apache.solr.schema.ExchangeRateProvider;
 import org.apache.solr.schema.FieldType;
 import org.apache.solr.schema.PointField;
 import org.apache.solr.schema.SchemaField;
@@ -120,6 +123,14 @@ class FacetRangeProcessor extends FacetProcessor<FacetRange> {
     }
   }
 
+  /**
+   * Returns a {@link Calc} instance to use for <em>term</em> faceting over a numeric field.
+   * This metod is unused for <code>range</code> faceting, and exists solely as a helper method for other classes
+   * 
+   * @param sf A field to facet on, must be of a type such that {@link FieldType#getNumberType} is non null
+   * @return a <code>Calc</code> instance with {@link Calc#bitsToValue} and {@link Calc#bitsToSortableBits} methods suitable for the specified field.
+   * @see FacetFieldProcessorByHashDV
+   */
   public static Calc getNumericCalc(SchemaField sf) {
     Calc calc;
     final FieldType ft = sf.getType();
@@ -203,6 +214,8 @@ class FacetRangeProcessor extends FacetProcessor<FacetRange> {
               (SolrException.ErrorCode.BAD_REQUEST,
                   "Unable to range facet on tried field of unexpected type:" + freq.field);
       }
+    } else if (ft instanceof CurrencyFieldType) {
+      calc = new CurrencyCalc(sf);
     } else {
       throw new SolrException
           (SolrException.ErrorCode.BAD_REQUEST,
@@ -260,7 +273,7 @@ class FacetRangeProcessor extends FacetProcessor<FacetRange> {
               (include.contains(FacetParams.FacetRangeInclude.EDGE) &&
                   0 == high.compareTo(end)));
 
-      Range range = new Range(low, low, high, incLower, incUpper);
+      Range range = new Range(calc.buildRangeLabel(low), low, high, incLower, incUpper);
       rangeList.add( range );
 
       low = high;
@@ -400,15 +413,29 @@ class FacetRangeProcessor extends FacetProcessor<FacetRange> {
       this.field = field;
     }
 
+    /**
+     * Used by {@link FacetFieldProcessorByHashDV} for field faceting on numeric types -- not used for <code>range</code> faceting
+     */
     public Comparable bitsToValue(long bits) {
       return bits;
     }
 
+    /**
+     * Used by {@link FacetFieldProcessorByHashDV} for field faceting on numeric types -- not used for <code>range</code> faceting
+     */
     public long bitsToSortableBits(long bits) {
       return bits;
     }
 
     /**
+     * Given the low value for a bucket, generates the appropraite "label" object to use. 
+     * By default return the low object unmodified.
+     */
+    public Object buildRangeLabel(Comparable low) {
+      return low;
+    }
+    
+    /**
      * Formats a value into a label used in a response
      * Default Impl just uses toString()
      */
@@ -605,6 +632,84 @@ class FacetRangeProcessor extends FacetProcessor<FacetRange> {
     }
   }
 
+  private static class CurrencyCalc extends Calc {
+    private String defaultCurrencyCode;
+    private ExchangeRateProvider exchangeRateProvider;
+    public CurrencyCalc(final SchemaField field) {
+      super(field);
+      if(!(this.field.getType() instanceof CurrencyFieldType)) {
+        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
+                                "Cannot perform range faceting over non CurrencyField fields");
+      }
+      defaultCurrencyCode =
+        ((CurrencyFieldType)this.field.getType()).getDefaultCurrency();
+      exchangeRateProvider =
+        ((CurrencyFieldType)this.field.getType()).getProvider();
+    }
+
+    /** 
+     * Throws a Server Error that this type of operation is not supported for this field 
+     * {@inheritDoc} 
+     */
+    @Override
+    public Comparable bitsToValue(long bits) {
+      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
+                              "Currency Field " + field.getName() + " can not be used in this way");
+    }
+
+    /** 
+     * Throws a Server Error that this type of operation is not supported for this field 
+     * {@inheritDoc} 
+     */
+    @Override
+    public long bitsToSortableBits(long bits) {
+      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
+                              "Currency Field " + field.getName() + " can not be used in this way");
+    }
+
+    /**
+     * Returns the short string representation of the CurrencyValue
+     * @see CurrencyValue#strValue
+     */
+    @Override
+    public Object buildRangeLabel(Comparable low) {
+      return ((CurrencyValue)low).strValue();
+    }
+    
+    @Override
+    public String formatValue(Comparable val) {
+      return ((CurrencyValue)val).strValue();
+    }
+
+    @Override
+    protected Comparable parseStr(final String rawval) throws java.text.ParseException {
+      return CurrencyValue.parse(rawval, defaultCurrencyCode);
+    }
+
+    @Override
+    protected Object parseGap(final String rawval) throws java.text.ParseException {
+      return parseStr(rawval);
+    }
+
+    @Override
+    protected Comparable parseAndAddGap(Comparable value, String gap) throws java.text.ParseException{
+      if (value == null) {
+        throw new NullPointerException("Cannot perform range faceting on null CurrencyValue");
+      }
+      CurrencyValue val = (CurrencyValue) value;
+      CurrencyValue gapCurrencyValue =
+        CurrencyValue.parse(gap, defaultCurrencyCode);
+      long gapAmount =
+        CurrencyValue.convertAmount(this.exchangeRateProvider,
+                                    gapCurrencyValue.getCurrencyCode(),
+                                    gapCurrencyValue.getAmount(),
+                                    val.getCurrencyCode());
+      return new CurrencyValue(val.getAmount() + gapAmount,
+                               val.getCurrencyCode());
+
+    }
+
+  }
 
   // this refineFacets method is patterned after FacetFieldProcessor.refineFacets and should
   // probably be merged when range facet becomes more like field facet in it's ability to sort and limit
@@ -709,16 +814,14 @@ class FacetRangeProcessor extends FacetProcessor<FacetRange> {
             (include.contains(FacetParams.FacetRangeInclude.EDGE) &&
                 0 == high.compareTo(end)));
 
-    Range range = new Range(low, low, high, incLower, incUpper);
+    Range range = new Range(calc.buildRangeLabel(low), low, high, incLower, incUpper);
 
 
     // now refine this range
 
     SimpleOrderedMap<Object> bucket = new SimpleOrderedMap<>();
-    FieldType ft = sf.getType();
-    bucket.add("val", range.low); // use "low" instead of bucketVal because it will be the right type (we may have been passed back long instead of int for example)
-    // String internal = ft.toInternal( tobj.toString() );  // TODO - we need a better way to get from object to query...
-
+    bucket.add("val", range.label);
+    
     Query domainQ = sf.getType().getRangeQuery(null, sf, range.low == null ? null : calc.formatValue(range.low), range.high==null ? null : calc.formatValue(range.high), range.includeLower, range.includeUpper);
     fillBucket(bucket, domainQ, null, skip, facetInfo);
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/6dcbb2d4/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTypeTest.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTypeTest.java b/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTypeTest.java
index c2f8f2d..c50f5c5 100644
--- a/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTypeTest.java
+++ b/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTypeTest.java
@@ -26,8 +26,11 @@ import java.util.Set;
 import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
 import org.apache.lucene.index.IndexableField;
 import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.core.SolrCore;
 import org.apache.solr.util.RTimer;
+
 import org.junit.Assume;
 import org.junit.BeforeClass;
 import org.junit.Ignore;
@@ -459,6 +462,238 @@ public class CurrencyFieldTypeTest extends SolrTestCaseJ4 {
   }
 
   @Test
+  public void testStringValue() throws Exception {
+    assertEquals("3.14,USD", new CurrencyValue(314, "USD").strValue());
+    assertEquals("-3.14,GBP", new CurrencyValue(-314, "GBP").strValue());
+    assertEquals("3.14,GBP", new CurrencyValue(314, "GBP").strValue());
+
+    CurrencyValue currencyValue = new CurrencyValue(314, "XYZ");
+    try {
+      String string = currencyValue.strValue();
+      fail("Expected SolrException");
+    } catch (SolrException exception) {
+    } catch (Throwable throwable) {
+      fail("Expected SolrException");
+    }
+  }
+
+  @Test
+  public void testRangeFacet() throws Exception {
+    assumeTrue("This test is only applicable to the XML file based exchange rate provider " +
+               "because it excercies the asymetric exchange rates option it supports",
+               expectedProviderClass.equals(FileExchangeRateProvider.class));
+    
+    clearIndex();
+    
+    // NOTE: in our test conversions EUR uses an asynetric echange rate
+    // these are the equivilent values when converting to:     USD        EUR        GBP
+    assertU(adoc("id", "" + 1, fieldName, "10.00,USD"));   // 10.00,USD  25.00,EUR   5.00,GBP
+    assertU(adoc("id", "" + 2, fieldName, "15.00,EUR"));   //  7.50,USD  15.00,EUR   7.50,GBP
+    assertU(adoc("id", "" + 3, fieldName, "6.00,GBP"));    // 12.00,USD  12.00,EUR   6.00,GBP
+    assertU(adoc("id", "" + 4, fieldName, "7.00,EUR"));    //  3.50,USD   7.00,EUR   3.50,GBP
+    assertU(adoc("id", "" + 5, fieldName, "2,GBP"));       //  4.00,USD   4.00,EUR   2.00,GBP
+    assertU(commit());
+
+    for (String suffix : Arrays.asList("", ",USD")) {
+      assertQ("Ensure that we get correct facet counts back in USD (explicit or implicit default) (facet.range)",
+              req("fl", "*,score", "q", "*:*", "rows", "0", "facet", "true",
+                  "facet.range", fieldName,
+                  "f." + fieldName + ".facet.range.start", "4.00" + suffix,
+                  "f." + fieldName + ".facet.range.end", "11.00" + suffix,
+                  "f." + fieldName + ".facet.range.gap", "1.00" + suffix,
+                  "f." + fieldName + ".facet.range.other", "all")
+              ,"count(//lst[@name='counts']/int)=7"
+              ,"//lst[@name='counts']/int[@name='4.00,USD']='1'"
+              ,"//lst[@name='counts']/int[@name='5.00,USD']='0'"
+              ,"//lst[@name='counts']/int[@name='6.00,USD']='0'"
+              ,"//lst[@name='counts']/int[@name='7.00,USD']='1'"
+              ,"//lst[@name='counts']/int[@name='8.00,USD']='0'"
+              ,"//lst[@name='counts']/int[@name='9.00,USD']='0'"
+              ,"//lst[@name='counts']/int[@name='10.00,USD']='1'"
+              ,"//int[@name='after']='1'"
+              ,"//int[@name='before']='1'"
+              ,"//int[@name='between']='3'"
+              );
+      assertQ("Ensure that we get correct facet counts back in USD (explicit or implicit default) (json.facet)",
+              req("fl", "*,score", "q", "*:*", "rows", "0", "json.facet",
+                  "{ xxx : { type:range, field:" + fieldName + ", " +
+                  "          start:'4.00"+suffix+"', gap:'1.00"+suffix+"', end:'11.00"+suffix+"', other:all } }")
+              ,"count(//lst[@name='xxx']/arr[@name='buckets']/lst)=7"
+              ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='4.00,USD']]"
+              ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='5.00,USD']]"
+              ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='6.00,USD']]"
+              ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='7.00,USD']]"
+              ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='8.00,USD']]"
+              ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='9.00,USD']]"
+              ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='10.00,USD']]"
+              ,"//lst[@name='xxx']/lst[@name='before' ]/int[@name='count'][.='1']"
+              ,"//lst[@name='xxx']/lst[@name='after'  ]/int[@name='count'][.='1']"
+              ,"//lst[@name='xxx']/lst[@name='between']/int[@name='count'][.='3']"
+              );
+    }
+
+    assertQ("Zero value as start range point + mincount (facet.range)",
+            req("fl", "*,score", "q", "*:*", "rows", "0", "facet", "true", "facet.mincount", "1",
+                "facet.range", fieldName,
+                "f." + fieldName + ".facet.range.start", "0,USD",
+                "f." + fieldName + ".facet.range.end", "11.00,USD",
+                "f." + fieldName + ".facet.range.gap", "1.00,USD",
+                "f." + fieldName + ".facet.range.other", "all")
+            ,"count(//lst[@name='counts']/int)=4"
+            ,"//lst[@name='counts']/int[@name='3.00,USD']='1'"
+            ,"//lst[@name='counts']/int[@name='4.00,USD']='1'"
+            ,"//lst[@name='counts']/int[@name='7.00,USD']='1'"
+            ,"//lst[@name='counts']/int[@name='10.00,USD']='1'"
+            ,"//int[@name='before']='0'"
+            ,"//int[@name='after']='1'"
+            ,"//int[@name='between']='4'"
+            );
+    assertQ("Zero value as start range point + mincount (json.facet)", 
+            req("fl", "*,score", "q", "*:*", "rows", "0", "json.facet",
+                "{ xxx : { type:range, mincount:1, field:" + fieldName +
+                ", start:'0.00,USD', gap:'1.00,USD', end:'11.00,USD', other:all } }")
+            ,"count(//lst[@name='xxx']/arr[@name='buckets']/lst)=4"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='3.00,USD']]"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='4.00,USD']]"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='7.00,USD']]"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='10.00,USD']]"
+            ,"//lst[@name='xxx']/lst[@name='before' ]/int[@name='count'][.='0']"
+            ,"//lst[@name='xxx']/lst[@name='after'  ]/int[@name='count'][.='1']"
+            ,"//lst[@name='xxx']/lst[@name='between']/int[@name='count'][.='4']"
+            );
+
+    // NOTE: because of asymetric EUR exchange rate, these buckets are diff then the similar looking USD based request above
+    // This request converts the values in each doc into EUR to decide what range buck it's in.
+    assertQ("Ensure that we get correct facet counts back in EUR (facet.range)",
+            req("fl", "*,score", "q", "*:*", "rows", "0", "facet", "true",
+                "facet.range", fieldName,
+                "f." + fieldName + ".facet.range.start", "8.00,EUR",
+                "f." + fieldName + ".facet.range.end", "22.00,EUR",
+                "f." + fieldName + ".facet.range.gap", "2.00,EUR",
+                "f." + fieldName + ".facet.range.other", "all"
+                )
+            , "count(//lst[@name='counts']/int)=7"
+            , "//lst[@name='counts']/int[@name='8.00,EUR']='0'"
+            , "//lst[@name='counts']/int[@name='10.00,EUR']='0'"
+            , "//lst[@name='counts']/int[@name='12.00,EUR']='1'"
+            , "//lst[@name='counts']/int[@name='14.00,EUR']='1'"
+            , "//lst[@name='counts']/int[@name='16.00,EUR']='0'"
+            , "//lst[@name='counts']/int[@name='18.00,EUR']='0'"
+            , "//lst[@name='counts']/int[@name='20.00,EUR']='0'"
+            , "//int[@name='before']='2'"
+            , "//int[@name='after']='1'"
+            , "//int[@name='between']='2'"
+            );
+    assertQ("Ensure that we get correct facet counts back in EUR (json.facet)",
+            req("fl", "*,score", "q", "*:*", "rows", "0", "json.facet",
+                "{ xxx : { type:range, field:" + fieldName + ", start:'8.00,EUR', gap:'2.00,EUR', end:'22.00,EUR', other:all } }")
+            ,"count(//lst[@name='xxx']/arr[@name='buckets']/lst)=7"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='8.00,EUR']]"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='10.00,EUR']]"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='12.00,EUR']]"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='14.00,EUR']]"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='16.00,EUR']]"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='18.00,EUR']]"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='20.00,EUR']]"
+            ,"//lst[@name='xxx']/lst[@name='before' ]/int[@name='count'][.='2']"
+            ,"//lst[@name='xxx']/lst[@name='after'  ]/int[@name='count'][.='1']"
+            ,"//lst[@name='xxx']/lst[@name='between']/int[@name='count'][.='2']"
+            );
+
+    
+    // GBP has a symetric echange rate with USD, so these counts are *similar* to the USD based request above...
+    // but the asymetric EUR/USD rate means that when computing counts realtive to GBP the EUR based docs wind up in
+    // diff buckets
+    assertQ("Ensure that we get correct facet counts back in GBP (facet.range)",
+            req("fl", "*,score", "q", "*:*", "rows", "0", "facet", "true",
+                "facet.range", fieldName,
+                "f." + fieldName + ".facet.range.start", "2.00,GBP",
+                "f." + fieldName + ".facet.range.end", "5.50,GBP",
+                "f." + fieldName + ".facet.range.gap", "0.50,GBP",
+                "f." + fieldName + ".facet.range.other", "all"
+                )
+            , "count(//lst[@name='counts']/int)=7"
+            , "//lst[@name='counts']/int[@name='2.00,GBP']='1'"
+            , "//lst[@name='counts']/int[@name='2.50,GBP']='0'"
+            , "//lst[@name='counts']/int[@name='3.00,GBP']='0'"
+            , "//lst[@name='counts']/int[@name='3.50,GBP']='1'"
+            , "//lst[@name='counts']/int[@name='4.00,GBP']='0'"
+            , "//lst[@name='counts']/int[@name='4.50,GBP']='0'"
+            , "//lst[@name='counts']/int[@name='5.00,GBP']='1'"
+            , "//int[@name='before']='0'"
+            , "//int[@name='after']='2'"
+            , "//int[@name='between']='3'"
+            );
+    assertQ("Ensure that we get correct facet counts back in GBP (json.facet)",
+            req("fl", "*,score", "q", "*:*", "rows", "0", "json.facet",
+                "{ xxx : { type:range, field:" + fieldName + ", start:'2.00,GBP', gap:'0.50,GBP', end:'5.50,GBP', other:all } }")
+            ,"count(//lst[@name='xxx']/arr[@name='buckets']/lst)=7"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='2.00,GBP']]"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='2.50,GBP']]"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='3.00,GBP']]"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='3.50,GBP']]"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='4.00,GBP']]"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='4.50,GBP']]"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='5.00,GBP']]"
+            ,"//lst[@name='xxx']/lst[@name='before' ]/int[@name='count'][.='0']"
+            ,"//lst[@name='xxx']/lst[@name='after'  ]/int[@name='count'][.='2']"
+            ,"//lst[@name='xxx']/lst[@name='between']/int[@name='count'][.='3']"
+            );
+
+    assertQ("Ensure that we can set a gap in a currency other than the start and end currencies (facet.range)",
+            req("fl", "*,score", "q", "*:*", "rows", "0", "facet", "true",
+                "facet.range", fieldName,
+                "f." + fieldName + ".facet.range.start", "4.00,USD",
+                "f." + fieldName + ".facet.range.end", "11.00,USD",
+                "f." + fieldName + ".facet.range.gap", "0.50,GBP",
+                "f." + fieldName + ".facet.range.other", "all"
+                )
+            , "count(//lst[@name='counts']/int)=7"
+            , "//lst[@name='counts']/int[@name='4.00,USD']='1'"
+            , "//lst[@name='counts']/int[@name='5.00,USD']='0'"
+            , "//lst[@name='counts']/int[@name='6.00,USD']='0'"
+            , "//lst[@name='counts']/int[@name='7.00,USD']='1'"
+            , "//lst[@name='counts']/int[@name='8.00,USD']='0'"
+            , "//lst[@name='counts']/int[@name='9.00,USD']='0'"
+            , "//lst[@name='counts']/int[@name='10.00,USD']='1'"
+            , "//int[@name='before']='1'"
+            , "//int[@name='after']='1'"
+            , "//int[@name='between']='3'"
+            );
+    assertQ("Ensure that we can set a gap in a currency other than the start and end currencies (json.facet)",
+            req("fl", "*,score", "q", "*:*", "rows", "0", "json.facet",
+                "{ xxx : { type:range, field:" + fieldName + ", start:'4.00,USD', gap:'0.50,GBP', end:'11.00,USD', other:all } }")
+            ,"count(//lst[@name='xxx']/arr[@name='buckets']/lst)=7"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='4.00,USD']]"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='5.00,USD']]"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='6.00,USD']]"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='7.00,USD']]"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='8.00,USD']]"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='9.00,USD']]"
+            ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='10.00,USD']]"
+            
+            ,"//lst[@name='xxx']/lst[@name='before' ]/int[@name='count'][.='1']"
+            ,"//lst[@name='xxx']/lst[@name='after'  ]/int[@name='count'][.='1']"
+            ,"//lst[@name='xxx']/lst[@name='between']/int[@name='count'][.='3']"
+            );
+
+    for (SolrParams facet : Arrays.asList(params("facet", "true",
+                                                 "facet.range", fieldName,
+                                                 "f." + fieldName + ".facet.range.start", "4.00,USD",
+                                                 "f." + fieldName + ".facet.range.end", "11.00,EUR",
+                                                 "f." + fieldName + ".facet.range.gap", "1.00,USD",
+                                                 "f." + fieldName + ".facet.range.other", "all"),
+                                          params("json.facet",
+                                                 "{ xxx : { type:range, field:" + fieldName + ", start:'4.00,USD', " +
+                                                 "          gap:'1.00,USD', end:'11.00,EUR', other:all } }"))) {
+      assertQEx("Ensure that we throw an error if we try to use different start and end currencies",
+                "Cannot compare CurrencyValues when their currencies are not equal", 
+                req(facet, "q", "*:*"),
+                SolrException.ErrorCode.BAD_REQUEST);
+    }
+  }
+
+  @Test
   public void testMockFieldType() throws Exception {
     assumeTrue("This test is only applicable to the mock exchange rate provider",
         expectedProviderClass.equals(MockExchangeRateProvider.class));

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/6dcbb2d4/solr/core/src/test/org/apache/solr/search/CurrencyRangeFacetCloudTest.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/search/CurrencyRangeFacetCloudTest.java b/solr/core/src/test/org/apache/solr/search/CurrencyRangeFacetCloudTest.java
new file mode 100644
index 0000000..c4b9281
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/search/CurrencyRangeFacetCloudTest.java
@@ -0,0 +1,483 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.search;
+
+import java.lang.invoke.MethodHandles;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.lucene.util.TestUtil;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.UpdateRequest;
+import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.client.solrj.response.RangeFacet;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CoreAdminParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.schema.CurrencyFieldTypeTest;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class CurrencyRangeFacetCloudTest extends SolrCloudTestCase {
+
+  private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+  
+  private static final String COLLECTION = MethodHandles.lookup().lookupClass().getName();
+  private static final String CONF = COLLECTION + "_configSet";
+  
+  private static String FIELD = null; // randomized
+
+  private static final List<String> STR_VALS = Arrays.asList("x0", "x1", "x2");
+  // NOTE: in our test conversions EUR uses an asynetric echange rate
+  // these are the equivilent values relative to:                                USD        EUR        GBP
+  private static final List<String> VALUES = Arrays.asList("10.00,USD",     // 10.00,USD  25.00,EUR   5.00,GBP
+                                                           "15.00,EUR",     //  7.50,USD  15.00,EUR   7.50,GBP
+                                                           "6.00,GBP",      // 12.00,USD  12.00,EUR   6.00,GBP
+                                                           "7.00,EUR",      //  3.50,USD   7.00,EUR   3.50,GBP
+                                                           "2,GBP");        //  4.00,USD   4.00,EUR   2.00,GBP
+  private static final int NUM_DOCS = STR_VALS.size() * VALUES.size();
+  
+  @BeforeClass
+  public static void setupCluster() throws Exception {
+    CurrencyFieldTypeTest.assumeCurrencySupport("USD", "EUR", "MXN", "GBP", "JPY", "NOK");
+    FIELD = usually() ? "amount_CFT" : "amount";
+    
+    final int numShards = TestUtil.nextInt(random(),1,5);
+    final int numReplicas = 1;
+    final int maxShardsPerNode = 1;
+    final int nodeCount = numShards * numReplicas;
+
+    configureCluster(nodeCount)
+      .addConfig(CONF, Paths.get(TEST_HOME(), "collection1", "conf"))
+      .configure();
+
+    assertEquals(0, (CollectionAdminRequest.createCollection(COLLECTION, CONF, numShards, numReplicas)
+                     .setMaxShardsPerNode(maxShardsPerNode)
+                     .setProperties(Collections.singletonMap(CoreAdminParams.CONFIG, "solrconfig-minimal.xml"))
+                     .process(cluster.getSolrClient())).getStatus());
+    
+    cluster.getSolrClient().setDefaultCollection(COLLECTION);
+    
+    for (int id = 0; id < NUM_DOCS; id++) { // we're indexing each Currency value in 3 docs, each with a diff 'x_s' field value
+      // use modulo to pick the values, so we don't add the docs in strict order of either VALUES of STR_VALS
+      // (that way if we want ot filter by id later, it's an independent variable)
+      final String x = STR_VALS.get(id % STR_VALS.size());
+      final String val = VALUES.get(id % VALUES.size());
+      assertEquals(0, (new UpdateRequest().add(sdoc("id", "" + id,
+                                                    "x_s", x,
+                                                    FIELD, val))
+                       ).process(cluster.getSolrClient()).getStatus());
+      
+    }
+    assertEquals(0, cluster.getSolrClient().commit().getStatus());
+  }
+
+  public void testSimpleRangeFacetsOfSymetricRates() throws Exception {
+
+    for (boolean use_mincount : Arrays.asList(true, false)) {
+    
+      // exchange rates relative to USD...
+      //
+      // for all of these permutations, the numDocs in each bucket that we get back should be the same
+      // (regardless of the any asymetric echanges ranges, or the currency used for the 'gap') because the
+      // start & end are always in USD.
+      //
+      // NOTE:
+      //  - 0,1,2 are the *input* start,gap,end
+      //  - 3,4,5 are the *normalized* start,gap,end expected in the response
+      for (List<String> args : Arrays.asList(// default currency is USD
+                                             Arrays.asList("4", "1.00", "11.0",
+                                                           "4.00,USD", "1.00,USD", "11.00,USD"),
+                                             // explicit USD
+                                             Arrays.asList("4,USD", "1,USD", "11,USD",
+                                                           "4.00,USD", "1.00,USD", "11.00,USD"),
+                                             // Gap can be in diff currency (but start/end must currently match)
+                                             Arrays.asList("4.00,USD", "000.50,GBP", "11,USD",
+                                                           "4.00,USD", ".50,GBP", "11.00,USD"),
+                                             Arrays.asList("4.00,USD", "2,EUR", "11,USD",
+                                                           "4.00,USD", "2.00,EUR", "11.00,USD"))) {
+        
+        assertEquals(6, args.size()); // sanity check
+        
+        // first let's check facet.range
+        SolrQuery solrQuery = new SolrQuery("q", "*:*", "rows", "0", "facet", "true", "facet.range", FIELD,
+                                            "facet.mincount", (use_mincount ? "3" : "0"),
+                                            "f." + FIELD + ".facet.range.start", args.get(0),
+                                            "f." + FIELD + ".facet.range.gap", args.get(1),
+                                            "f." + FIELD + ".facet.range.end", args.get(2),
+                                            "f." + FIELD + ".facet.range.other", "all");
+        QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
+        try {
+          assertEquals(NUM_DOCS, rsp.getResults().getNumFound());
+          
+          final String start = args.get(3);
+          final String gap = args.get(4);
+          final String end = args.get(5);
+          
+          final List<RangeFacet> range_facets = rsp.getFacetRanges();
+          assertEquals(1, range_facets.size());
+          final RangeFacet result = range_facets.get(0);
+          assertEquals(FIELD, result.getName());
+          assertEquals(start, result.getStart());
+          assertEquals(gap, result.getGap());
+          assertEquals(end, result.getEnd());
+          assertEquals(3, result.getBefore());
+          assertEquals(3, result.getAfter());
+          assertEquals(9, result.getBetween());
+          
+          List<RangeFacet.Count> counts = result.getCounts();
+          if (use_mincount) {
+            assertEquals(3, counts.size());
+            for (int i = 0; i < 3; i++) {
+              RangeFacet.Count bucket = counts.get(i);
+              assertEquals((4 + (i * 3)) + ".00,USD", bucket.getValue());
+              assertEquals("bucket #" + i, 3, bucket.getCount());
+            }
+          } else {
+            assertEquals(7, counts.size());
+            for (int i = 0; i < 7; i++) {
+              RangeFacet.Count bucket = counts.get(i);
+              assertEquals((4 + i) + ".00,USD", bucket.getValue());
+              assertEquals("bucket #" + i, (i == 0 || i == 3 || i == 6) ? 3 : 0, bucket.getCount());
+            }
+          }
+        } catch (AssertionError|RuntimeException ae) {
+          throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
+        }
+
+        // same basic logic, w/json.facet
+        solrQuery = new SolrQuery("q", "*:*", "rows", "0", "json.facet",
+                                  "{ foo:{ type:range, field:"+FIELD+", mincount:"+(use_mincount ? 3 : 0)+", " +
+                                  "        start:'"+args.get(0)+"', gap:'"+args.get(1)+"', end:'"+args.get(2)+"', other:all}}");
+        rsp = cluster.getSolrClient().query(solrQuery);
+        try {
+          assertEquals(NUM_DOCS, rsp.getResults().getNumFound());
+          
+          final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
+          
+          assertEqualsHACK("before", 3L, ((NamedList)foo.get("before")).get("count"));
+          assertEqualsHACK("after", 3L, ((NamedList)foo.get("after")).get("count"));
+          assertEqualsHACK("between", 9L, ((NamedList)foo.get("between")).get("count"));
+          
+          final List<NamedList> buckets = (List<NamedList>) foo.get("buckets");
+          
+          if (use_mincount) {
+            assertEquals(3, buckets.size());
+            for (int i = 0; i < 3; i++) {
+              NamedList bucket = buckets.get(i);
+              assertEquals((4 + (3 * i)) + ".00,USD", bucket.get("val"));
+              assertEqualsHACK("bucket #" + i, 3L, bucket.get("count"));
+            }
+          } else {
+            assertEquals(7, buckets.size());
+            for (int i = 0; i < 7; i++) {
+              NamedList bucket = buckets.get(i);
+              assertEquals((4 + i) + ".00,USD", bucket.get("val"));
+              assertEqualsHACK("bucket #" + i, (i == 0 || i == 3 || i == 6) ? 3L : 0L, bucket.get("count"));
+            }
+          }
+        } catch (AssertionError|RuntimeException ae) {
+          throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
+        }
+        
+      }
+    }
+  }
+      
+  public void testFacetRangeOfAsymetricRates() throws Exception {
+    // facet.range: exchange rates relative to EUR...
+    //
+    // because of the asymetric echange rate, the counts for these buckets will be different
+    // then if we just converted the EUR values to USD
+    for (boolean use_mincount : Arrays.asList(true, false)) {
+      final SolrQuery solrQuery = new SolrQuery("q", "*:*", "rows", "0", "facet", "true", "facet.range", FIELD,
+                                                "facet.mincount", (use_mincount ? "3" : "0"),
+                                                "f." + FIELD + ".facet.range.start", "8,EUR",
+                                                "f." + FIELD + ".facet.range.gap", "2,EUR",
+                                                "f." + FIELD + ".facet.range.end", "22,EUR",
+                                                "f." + FIELD + ".facet.range.other", "all");
+      final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
+      try {
+        assertEquals(NUM_DOCS, rsp.getResults().getNumFound());
+        final List<RangeFacet> range_facets = rsp.getFacetRanges();
+        assertEquals(1, range_facets.size());
+        final RangeFacet result = range_facets.get(0);
+        assertEquals(FIELD, result.getName());
+        assertEquals("8.00,EUR", result.getStart());
+        assertEquals("2.00,EUR", result.getGap());
+        assertEquals("22.00,EUR", result.getEnd());
+        assertEquals(6, result.getBefore());
+        assertEquals(3, result.getAfter());
+        assertEquals(6, result.getBetween());
+        
+        List<RangeFacet.Count> counts = result.getCounts();
+        if (use_mincount) {
+          assertEquals(2, counts.size());
+          for (int i = 0; i < 2; i++) {
+            RangeFacet.Count bucket = counts.get(i);
+            assertEquals((12 + (i * 2)) + ".00,EUR", bucket.getValue());
+            assertEquals("bucket #" + i, 3, bucket.getCount());
+          }
+        } else {
+          assertEquals(7, counts.size());
+          for (int i = 0; i < 7; i++) {
+            RangeFacet.Count bucket = counts.get(i);
+            assertEquals((8 + (i * 2)) + ".00,EUR", bucket.getValue());
+            assertEquals("bucket #" + i, (i == 2 || i == 3) ? 3 : 0, bucket.getCount());
+          }
+        }
+      } catch (AssertionError|RuntimeException ae) {
+        throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
+      }
+    }
+  }
+  
+  public void testJsonFacetRangeOfAsymetricRates() throws Exception {
+    // json.facet: exchange rates relative to EUR (same as testFacetRangeOfAsymetricRates)
+    //
+    // because of the asymetric echange rate, the counts for these buckets will be different
+    // then if we just converted the EUR values to USD
+    for (boolean use_mincount : Arrays.asList(true, false)) {
+      final SolrQuery solrQuery = new SolrQuery("q", "*:*", "rows", "0", "json.facet",
+                                                "{ foo:{ type:range, field:"+FIELD+", start:'8,EUR', " +
+                                                "        mincount:"+(use_mincount ? 3 : 0)+", " +
+                                                "        gap:'2,EUR', end:'22,EUR', other:all}}");
+      final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
+      try {
+        assertEquals(NUM_DOCS, rsp.getResults().getNumFound());
+        
+        final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
+        
+        assertEqualsHACK("before", 6L, ((NamedList)foo.get("before")).get("count"));
+        assertEqualsHACK("after", 3L, ((NamedList)foo.get("after")).get("count"));
+        assertEqualsHACK("between", 6L, ((NamedList)foo.get("between")).get("count"));
+        
+        final List<NamedList> buckets = (List<NamedList>) foo.get("buckets");
+        
+        if (use_mincount) {
+          assertEquals(2, buckets.size());
+          for (int i = 0; i < 2; i++) {
+            NamedList bucket = buckets.get(i);
+            assertEquals((12 + (i * 2)) + ".00,EUR", bucket.get("val"));
+            assertEqualsHACK("bucket #" + i, 3L, bucket.get("count"));
+          }
+        } else {
+          assertEquals(7, buckets.size());
+          for (int i = 0; i < 7; i++) {
+            NamedList bucket = buckets.get(i);
+            assertEquals((8 + (i * 2)) + ".00,EUR", bucket.get("val"));
+            assertEqualsHACK("bucket #" + i, (i == 2 || i == 3) ? 3L : 0L, bucket.get("count"));
+          }
+        }
+      } catch (AssertionError|RuntimeException ae) {
+        throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
+      }
+    }
+  }
+    
+  public void testFacetRangeCleanErrorOnMissmatchCurrency() {
+    final String expected = "Cannot compare CurrencyValues when their currencies are not equal";
+    ignoreException(expected);
+    
+    // test to check clean error when start/end have diff currency (facet.range)
+    final SolrQuery solrQuery = new SolrQuery("q", "*:*", "rows", "0", "facet", "true", "facet.range", FIELD,
+                                              "f." + FIELD + ".facet.range.start", "0,EUR",
+                                              "f." + FIELD + ".facet.range.gap", "10,EUR",
+                                              "f." + FIELD + ".facet.range.end", "100,USD");
+    final SolrException ex = expectThrows(SolrException.class, () -> {
+        final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
+      });
+    assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
+    assertTrue(ex.getMessage(), ex.getMessage().contains(expected));
+  }
+
+  public void testJsonFacetCleanErrorOnMissmatchCurrency() {
+    final String expected = "Cannot compare CurrencyValues when their currencies are not equal";
+    ignoreException(expected);
+    
+    // test to check clean error when start/end have diff currency (json.facet)
+    final SolrQuery solrQuery = new SolrQuery("q", "*:*", "json.facet",
+                                              "{ x:{ type:range, field:"+FIELD+", " +
+                                              "      start:'0,EUR', gap:'10,EUR', end:'100,USD' } }");
+    final SolrException ex = expectThrows(SolrException.class, () -> {
+        final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
+      });
+    assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
+    assertTrue(ex.getMessage(), ex.getMessage().contains(expected));
+  }
+  
+  @Test
+  public void testJsonRangeFacetWithSubFacet() throws Exception {
+
+    // range facet, with terms facet nested under using limit=2 w/overrequest disabled
+    // filter out the first 5 docs (by id) which should ensure that regardless of sharding:
+    //  - x2 being the top term for the 1st range bucket
+    //  - x0 being the top term for the 2nd range bucket
+    //  - the 2nd term in each bucket may vary based on shard/doc placement, but the count will always be '1'
+    // ...and in many cases (based on the shard/doc placement) this will require refinement to backfill the top terms
+    final String filter = "id_i1:["+VALUES.size()+" TO *]";
+
+    // the *facet* results should be the same regardless of wether we filter via fq, or using a domain filter on the top facet
+    for (boolean use_domain : Arrays.asList(true, false)) {
+      final String domain = use_domain ? "domain: { filter:'" + filter + "'}," : "";
+      final SolrQuery solrQuery = new SolrQuery("q", (use_domain ? "*:*" : filter),
+                                                "rows", "0", "json.facet",
+                                                "{ bar:{ type:range, field:"+FIELD+", " + domain + 
+                                                "        start:'0,EUR', gap:'10,EUR', end:'20,EUR', other:all " +
+                                                "        facet: { foo:{ type:terms, field:x_s, " +
+                                                "                       refine:true, limit:2, overrequest:0" +
+                                                " } } } }");
+      final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
+      try {
+        // this top level result count sanity check that should vary based on how we are filtering our facets...
+        assertEquals(use_domain ? 15 : 10, rsp.getResults().getNumFound());
+
+        final NamedList<Object> bar = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("bar");
+        final List<NamedList<Object>> bar_buckets = (List<NamedList<Object>>) bar.get("buckets");
+        final NamedList<Object> before = (NamedList<Object>) bar.get("before");
+        final NamedList<Object> between = (NamedList<Object>) bar.get("between");
+        final NamedList<Object> after = (NamedList<Object>) bar.get("after");
+        
+        // sanity check our high level expectations...
+        assertEquals("bar num buckets", 2, bar_buckets.size());
+        assertEqualsHACK("before count", 0L, before.get("count"));
+        assertEqualsHACK("between count", 8L, between.get("count"));
+        assertEqualsHACK("after count", 2L, after.get("count"));
+        
+        // drill into the various buckets...
+        
+        // before should have no subfacets since it's empty...
+        assertNull("before has foo???", before.get("foo"));
+        
+        // our 2 range buckets & their sub facets...
+        for (int i = 0; i < 2; i++) {
+          final NamedList<Object> bucket = bar_buckets.get(i);
+          assertEquals((i * 10) + ".00,EUR", bucket.get("val"));
+          assertEqualsHACK("bucket #" + i, 4L, bucket.get("count"));
+          final List<NamedList<Object>> foo_buckets = ((NamedList<List<NamedList<Object>>>)bucket.get("foo")).get("buckets");
+          assertEquals("bucket #" + i + " foo num buckets", 2, foo_buckets.size());
+          assertEquals("bucket #" + i + " foo top term", (0==i ? "x2" : "x0"), foo_buckets.get(0).get("val"));
+          assertEqualsHACK("bucket #" + i + " foo top count", 2, foo_buckets.get(0).get("count"));
+          // NOTE: we can't make any assertions about the 2nd val..
+          // our limit + randomized sharding could result in either remaining term being picked.
+          // but for eiter term, the count should be exactly the same...
+          assertEqualsHACK("bucket #" + i + " foo 2nd count", 1, foo_buckets.get(1).get("count"));
+        }
+        
+        { // between...
+          final List<NamedList<Object>> buckets = ((NamedList<List<NamedList<Object>>>)between.get("foo")).get("buckets");
+          assertEquals("between num buckets", 2, buckets.size());
+          // the counts should both be 3, and the term order should break the tie...
+          assertEquals("between bucket top", "x0", buckets.get(0).get("val"));
+          assertEqualsHACK("between bucket top count", 3L, buckets.get(0).get("count"));
+          assertEquals("between bucket 2nd", "x2", buckets.get(1).get("val"));
+          assertEqualsHACK("between bucket 2nd count", 3L, buckets.get(1).get("count"));
+        }
+        
+        { // after...
+          final List<NamedList<Object>> buckets = ((NamedList<List<NamedList<Object>>>)after.get("foo")).get("buckets");
+          assertEquals("after num buckets", 2, buckets.size());
+          // the counts should both be 1, and the term order should break the tie...
+          assertEquals("after bucket top", "x1", buckets.get(0).get("val"));
+          assertEqualsHACK("after bucket top count", 1L, buckets.get(0).get("count"));
+          assertEquals("after bucket 2nd", "x2", buckets.get(1).get("val"));
+          assertEqualsHACK("after bucket 2nd count", 1L, buckets.get(1).get("count"));
+        }
+        
+      } catch (AssertionError|RuntimeException ae) {
+        throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
+      }
+    }
+  }
+  
+  @Test
+  public void testJsonRangeFacetAsSubFacet() throws Exception {
+
+    // limit=1, overrequest=1, with refinement enabled
+    // filter out the first 5 docs (by id), which should ensure that 'x2' is the top bucket overall...
+    //   ...except in some rare sharding cases where it doesn't make it into the top 2 terms.
+    // 
+    // So the filter also explicitly accepts all 'x2' docs -- ensuring we have enough matches containing that term for it
+    // to be enough of a candidate in phase#1, but for many shard arrangements it won't be returned by all shards resulting
+    // in refinement being neccessary to get the x_s:x2 sub-shard ranges from shard(s) where x_s:x2 is only tied for the
+    // (shard local) top term count and would lose the (index order) tie breaker with x_s:x0 or x_s:x1
+    final String filter = "id_i1:["+VALUES.size()+" TO *] OR x_s:x2";
+
+    // the *facet* results should be the same regardless of wether we filter via fq, or using a domain filter on the top facet
+    for (boolean use_domain : Arrays.asList(true, false)) {
+      final String domain = use_domain ? "domain: { filter:'" + filter + "'}," : "";
+      final SolrQuery solrQuery = new SolrQuery("q", (use_domain ? "*:*" : filter),
+                                                "rows", "0", "json.facet",
+                                                "{ foo:{ type:terms, field:x_s, refine:true, limit:1, overrequest:1, " + domain +
+                                                "        facet: { bar:{ type:range, field:"+FIELD+", other:all, " +
+                                                "                       start:'8,EUR', gap:'2,EUR', end:'22,EUR' }} } }");
+      final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
+      try {
+        // this top level result count sanity check that should vary based on how we are filtering our facets...
+        assertEquals(use_domain ? 15 : 11, rsp.getResults().getNumFound());
+        
+        final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
+        
+        // sanity check...
+        // because of the facet limit, foo should only have 1 bucket
+        // because of the fq, the val should be "x2" and the count=5
+        final List<NamedList<Object>> foo_buckets = (List<NamedList<Object>>) foo.get("buckets");
+        assertEquals(1, foo_buckets.size());
+        assertEquals("x2", foo_buckets.get(0).get("val"));
+        assertEqualsHACK("foo bucket count", 5L, foo_buckets.get(0).get("count"));
+        
+        final NamedList<Object> bar = (NamedList<Object>)foo_buckets.get(0).get("bar");
+        
+        // these are the 'x2' specific counts, based on our fq...
+        
+        assertEqualsHACK("before", 2L, ((NamedList)bar.get("before")).get("count"));
+        assertEqualsHACK("after", 1L, ((NamedList)bar.get("after")).get("count"));
+        assertEqualsHACK("between", 2L, ((NamedList)bar.get("between")).get("count"));
+        
+        final List<NamedList> buckets = (List<NamedList>) bar.get("buckets");
+        assertEquals(7, buckets.size());
+        for (int i = 0; i < 7; i++) {
+          NamedList bucket = buckets.get(i);
+          assertEquals((8 + (i * 2)) + ".00,EUR", bucket.get("val"));
+          // 12,EUR & 15,EUR are the 2 values that align with x2 docs
+          assertEqualsHACK("bucket #" + i, (i == 2 || i == 3) ? 1L : 0L, bucket.get("count"));
+        }
+      } catch (AssertionError|RuntimeException ae) {
+        throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
+      }
+    }
+  }
+
+  /**
+   * HACK to work around SOLR-11775.
+   * Asserts that the 'actual' argument is a (non-null) Number, then compares it's 'longValue' to the 'expected' argument
+   */
+  private static void assertEqualsHACK(String msg, long expected, Object actual) {
+    assertNotNull(msg, actual);
+    assertTrue(msg + " ... NOT A NUMBER: " + actual.getClass(), Number.class.isInstance(actual));
+    assertEquals(msg, expected, ((Number)actual).longValue());
+  }
+  
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/6dcbb2d4/solr/server/solr/configsets/sample_techproducts_configs/conf/velocity/VM_global_library.vm
----------------------------------------------------------------------
diff --git a/solr/server/solr/configsets/sample_techproducts_configs/conf/velocity/VM_global_library.vm b/solr/server/solr/configsets/sample_techproducts_configs/conf/velocity/VM_global_library.vm
index 76516b7..ef2157c 100644
--- a/solr/server/solr/configsets/sample_techproducts_configs/conf/velocity/VM_global_library.vm
+++ b/solr/server/solr/configsets/sample_techproducts_configs/conf/velocity/VM_global_library.vm
@@ -173,7 +173,11 @@ $val##
 
 #macro(range_get_to_value $inval, $gapval)
 #if(${gapval.class.name} == "java.lang.String")
-$inval$gapval##
+#if($gapval.startsWith("+"))
+$inval$gapval## Typically date gaps start with +
+#else
+$inval+$gapval## If the gap does not start with a "+", we add it, such as for currency value
+#end
 #elseif(${gapval.class.name} == "java.lang.Float" || ${inval.class.name} == "java.lang.Float")
 $math.toDouble($math.add($inval,$gapval))##
 #else

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/6dcbb2d4/solr/solr-ref-guide/src/working-with-currencies-and-exchange-rates.adoc
----------------------------------------------------------------------
diff --git a/solr/solr-ref-guide/src/working-with-currencies-and-exchange-rates.adoc b/solr/solr-ref-guide/src/working-with-currencies-and-exchange-rates.adoc
index 5b1e23a..4ee5711 100644
--- a/solr/solr-ref-guide/src/working-with-currencies-and-exchange-rates.adoc
+++ b/solr/solr-ref-guide/src/working-with-currencies-and-exchange-rates.adoc
@@ -24,6 +24,7 @@ The `currency` FieldType provides support for monetary values to Solr/Lucene wit
 * Sorting
 * Currency parsing by either currency code or symbol
 * Symmetric & asymmetric exchange rates (asymmetric exchange rates are useful if there are fees associated with exchanging the currency)
+* Range faceting (using either `facet.range` or `type:range` in `json.facet`) as long as the `start` and `end` values are specified in the same Currency.
 
 == Configuring Currencies
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/6dcbb2d4/solr/solrj/src/java/org/apache/solr/client/solrj/response/QueryResponse.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/response/QueryResponse.java b/solr/solrj/src/java/org/apache/solr/client/solrj/response/QueryResponse.java
index 4e78005..3124a0b 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/response/QueryResponse.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/response/QueryResponse.java
@@ -384,7 +384,7 @@ public class QueryResponse extends SolrResponseBase
         Number between = (Number) values.get("between");
 
         rangeFacet = new RangeFacet.Numeric(facet.getKey(), start, end, gap, before, after, between);
-      } else {
+      } else if (rawGap instanceof String && values.get("start") instanceof Date) {
         String gap = (String) rawGap;
         Date start = (Date) values.get("start");
         Date end = (Date) values.get("end");
@@ -394,8 +394,18 @@ public class QueryResponse extends SolrResponseBase
         Number between = (Number) values.get("between");
 
         rangeFacet = new RangeFacet.Date(facet.getKey(), start, end, gap, before, after, between);
+      } else {
+        String gap = (String) rawGap;
+        String start = (String) values.get("start");
+        String end = (String) values.get("end");
+        
+        Number before = (Number) values.get("before");
+        Number after = (Number) values.get("after");
+        Number between = (Number) values.get("between");
+        
+        rangeFacet = new RangeFacet.Currency(facet.getKey(), start, end, gap, before, after, between);
       }
-
+      
       NamedList<Integer> counts = (NamedList<Integer>) values.get("counts");
       for (Map.Entry<String, Integer> entry : counts)   {
         rangeFacet.addCount(entry.getKey(), entry.getValue());

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/6dcbb2d4/solr/solrj/src/java/org/apache/solr/client/solrj/response/RangeFacet.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/response/RangeFacet.java b/solr/solrj/src/java/org/apache/solr/client/solrj/response/RangeFacet.java
index 6829c17..b970ef5 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/response/RangeFacet.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/response/RangeFacet.java
@@ -97,6 +97,12 @@ public abstract class RangeFacet<B, G> {
 
   }
 
+  public static class Currency extends RangeFacet<String, String> {
+    public Currency(String name, String start, String end, String gap, Number before, Number after, Number between) {
+      super(name, start, end, gap, before, after, between);
+    }
+  }
+
   public static class Count {
 
     private final String value;