-
-
Save robertdean/4636955 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Globalization; | |
using System.Linq; | |
using System.Linq.Expressions; | |
namespace BetterFacetedSearchAPI | |
{ | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
var facetList = new List<Facet> | |
{ | |
new FacetRange<TestClass, DateTime> | |
{ | |
Field = x => x.Date, | |
Ranges = | |
{ | |
Range.CreateLessThan(new DateTime(2008, 1, 1)), // NULL TO 1/1/2008 (less than) | |
Range.CreateGreaterThan(new DateTime(2008, 1, 22)), // 1/1/2008 TO NULL (greater than) | |
Range.Create(minValue: new DateTime(2008, 1, 1), maxValue: new DateTime(2008, 1, 2)) | |
} | |
}, | |
//Would like to be able to write something like this, but not sure if it's possible | |
//FacetRange.Create(x => x.Date, ranges: | |
// { | |
// Range.CreateLessThan(new DateTime(2008, 1, 1)), // NULL TO 1/1/2008 (less than) | |
// Range.CreateGreaterThan(new DateTime(2008, 1, 22)), // 1/1/2008 TO NULL (greater than) | |
// Range.Create(minValue: new DateTime(2008, 1, 1), maxValue: new DateTime(2008, 1, 2)) | |
// }), | |
new FacetRange<TestClass, int> | |
{ | |
Field = x => x.Number, | |
Ranges = | |
{ | |
Range.CreateLessThan(1001), // [NULL TO 1001] (less than) | |
Range.CreateGreaterThan(500), // [500 TO NULL] (greater than) | |
Range.Create(minValue: 10, maxValue: 100) | |
} | |
}, | |
new FacetRange<TestClass, string> | |
{ | |
Field = x => x.Id, | |
Ranges = | |
{ | |
Range.CreateLessThan("ddddd"), // [NULL TO ddddd] (less than) | |
Range.CreateGreaterThan("eeeee"), // [eeeee TO NULL] (greater than) | |
Range.Create(minValue: "aa", maxValue: "bb") | |
} | |
}, | |
new FacetRange<TestClass, string> { Field = x => x.Id } | |
}; | |
Facet firstItem = facetList.First(); | |
//Should print out "Date_Range" | |
Console.WriteLine(firstItem.Name); | |
//The expected ranges are | |
// [NULL TO 20080101000000] | |
// [20080122000000 TO NULL] | |
// [20080101000000 TO 20080102000000] | |
Console.WriteLine(String.Join("\n", firstItem.Ranges)); | |
} | |
} | |
class TestClass | |
{ | |
public string Id { get; set; } | |
public DateTime Date { get; set; } | |
public int Number { get; set; } | |
} | |
//Having this helper class doesn't seem to help with the type inference | |
public static class FacetRange | |
{ | |
public static FacetRange<T, U> Create<T, U>(Expression<Func<T, U>> field, List<Range<U>> ranges) | |
{ | |
return new FacetRange<T, U> | |
{ | |
Field = field, | |
Ranges = ranges | |
}; | |
} | |
} | |
public class FacetRange<T, U> | |
{ | |
public Expression<Func<T, U>> Field { get; set; } | |
public List<Range<U>> Ranges { get; set; } | |
public FacetRange() | |
{ | |
Ranges = new List<Range<U>>(); | |
} | |
private string GetFieldName() | |
{ | |
if (Field.Body.NodeType == ExpressionType.MemberAccess) | |
{ | |
//http://stackoverflow.com/questions/3049825/given-a-member-access-lambda-expression-convert-it-to-a-specific-string-represe | |
//This only works for 1 level, i.e. x => x.Date, NOT for x => x.Date.Now | |
MemberExpression body = (Field.Body.NodeType == ExpressionType.Convert) | |
? (MemberExpression)((UnaryExpression)Field.Body).Operand | |
: (MemberExpression)Field.Body; | |
return body.Member.Name; | |
} | |
throw new InvalidOperationException(String.Format("Unable to parse Expression: {0} - {1}", Field, Field.Body)); | |
} | |
public static implicit operator Facet(FacetRange<T, U> facetRange) | |
{ | |
return facetRange.ToSimpleFacet(); | |
} | |
private Facet ToSimpleFacet() | |
{ | |
var mode = (Ranges.Count == 0) ? FacetMode.Default : FacetMode.Ranges; | |
var nameSuffix = (mode == FacetMode.Ranges && typeof(U).FullName != "System.String") | |
? "_Range" | |
: String.Empty; | |
return new Facet | |
{ | |
Name = GetFieldName() + nameSuffix, | |
Mode = mode, | |
Ranges = (Ranges == null) | |
? new List<string>() | |
: Ranges.Select(x => x.ToString()).ToList() | |
}; | |
} | |
} | |
//This is just here to help with Genetic type inference (makes code nicer to write) | |
public static class Range | |
{ | |
public static Range<T> Create<T>(T minValue, T maxValue) | |
{ | |
return new Range<T>(minValue, maxValue); | |
} | |
public static Range<T> CreateLessThan<T>(T value) | |
{ | |
return new Range<T>(value, isLessThanRange: true); | |
} | |
public static Range<T> CreateGreaterThan<T>(T value) | |
{ | |
return new Range<T>(value, isLessThanRange: false); | |
} | |
} | |
public class Range<T> | |
{ | |
private readonly T _minValue; | |
private readonly bool _hasMinValue; | |
private readonly T _maxValue; | |
private readonly bool _hasMaxValue; | |
public Range(T minValue, T maxValue) | |
{ | |
//do some null checks??? | |
_minValue = minValue; | |
_hasMinValue = true; | |
_maxValue = maxValue; | |
_hasMaxValue = true; | |
} | |
public Range(T singleValue, bool isLessThanRange) | |
{ | |
if (isLessThanRange) | |
{ | |
//[NULL to value], i.e. results < value | |
_hasMinValue = false; | |
_hasMaxValue = true; | |
_maxValue = singleValue; | |
} | |
else | |
{ | |
//[value to NULL]. i.e. results > value | |
_hasMinValue = true; | |
_hasMaxValue = false; | |
_minValue = singleValue; | |
} | |
} | |
public override string ToString() | |
{ | |
//Once this code is in RavenDB, the calls to GetValue(..) below will be replaced with calls to NumberUtil.NumberToString(..) | |
//see https://github.com/ayende/ravendb/blob/master/Raven.Abstractions/Indexing/NumberUtil.cs#L14 | |
switch (typeof(T).FullName) | |
{ | |
//The nullable stuff here it a bit wierd, but it helps with trying to cast Value types | |
case "System.DateTime": | |
return ToStringHelper(GetValue(_minValue as DateTime?), GetValue(_maxValue as DateTime?)); | |
case "System.Int32": | |
return ToStringHelper(GetValue(_minValue as Int32?), GetValue(_maxValue as Int32?)); | |
case "System.Int64": | |
return ToStringHelper(GetValue(_minValue as Int64?), GetValue(_maxValue as Int64?)); | |
case "System.Single": | |
return ToStringHelper(GetValue(_minValue as Single?), GetValue(_maxValue as Single?)); | |
case "System.Double": | |
return ToStringHelper(GetValue(_minValue as Double?), GetValue(_maxValue as Double?)); | |
case "System.String": | |
return ToStringHelper(_minValue == null ? String.Empty : _minValue.ToString(), | |
_maxValue == null ? String.Empty : _maxValue.ToString()); | |
} | |
throw new InvalidOperationException("Unable to parse the given type " + typeof (T).Name + ", into a facet range!!! "); | |
} | |
private string ToStringHelper(string minValueAsText, string maxValueAsText) | |
{ | |
if (_hasMinValue && _hasMaxValue) | |
return String.Format("[{0} TO {1}]", minValueAsText, maxValueAsText); | |
else if (_hasMinValue) | |
return String.Format("[{0} TO NULL]", minValueAsText); | |
else if (_hasMaxValue) | |
return String.Format("[NULL TO {0}]", maxValueAsText); | |
throw new InvalidOperationException("Invalid combination of min/max values"); | |
} | |
private string GetValue(DateTime? value) | |
{ | |
return value.Value.ToString("yyyyMMddHHmmss"); | |
} | |
private string GetValue(Int32? value) //int | |
{ | |
return string.Format("0x{0:X8}", value.Value); | |
} | |
private string GetValue(Int64? value) //long | |
{ | |
return string.Format("0x{0:X16}", value.Value); | |
} | |
private string GetValue(Single? value) //float | |
{ | |
return "Fx" + value.Value.ToString("G", CultureInfo.InvariantCulture); | |
} | |
private string GetValue(Double? value) //double | |
{ | |
return "Dx" + value.Value.ToString("G", CultureInfo.InvariantCulture); | |
} | |
} | |
public class Facet | |
{ | |
public string Name { get; set; } | |
public FacetMode Mode { get; set; } | |
public List<string> Ranges { get; set; } | |
} | |
public enum FacetMode | |
{ | |
Default, | |
Ranges | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment