如何查询基于双列 "close to" 某个值的数据集?
How can I query a dataset based on double columns being "close to" some value?
我有一个参考数据库,其中包含天体网格上物体的坐标。我想查询数据库并找到给定点 "close to"(在一定 angular 距离内)的对象。
我试过这个查询:
const double WithinOneMinute = 1.0 / 60.0; // 1 minute of arc
var db = CompositionRoot.GetTargetDatabase();
var targets = from item in db.Targets
where item.RightAscension.IsCloseTo(ra.Value, WithinOneMinute)
&& item.Declination.IsCloseTo(dec.Value, WithinOneMinute)
select item;
var found = targets.ToList();
这失败了,因为 LINQ 查询提供程序不理解我的 IsCloseTo
扩展方法,该方法实现为:
public static bool IsCloseTo(this double comparand, double comparison, double tolerance = EightDecimalPlaces)
{
var difference = Math.Abs(comparand - comparison);
return (difference <= tolerance);
}
所以我目前正在寻找想法。有人做过这样的事吗?
LINQ(到 EF)查询提供程序不知道如何在 SQL 中执行您的 IsCloseTo 方法。您需要先枚举您的项目,然后使用您的扩展方法对其进行过滤,如下所示:
var db = CompositionRoot.GetTargetDatabase();
var targets = from item in db.Targets
select item;
//now targets will be enumarated and can be querable with LINQ to objects
var filteredTargets = from target in targets.ToList()
where target.RightAscension.IsCloseTo(ra.Value, WithinOneMinute)
&& target.Declination.IsCloseTo(dec.Value, WithinOneMinute)
select target;
var filteredTargets = targets.ToList();
问题是 Entity Framework 不知道如何将其翻译成 SQL。无需在进入数据库后进行过滤,您可以让查询包含过滤器,就像直接写 SQL 一样。它会更冗长一点,但是拖入大量数据会便宜得多,这些数据一旦内置在内存中就会立即被过滤。
您需要做的是将您的每个时间与您正在寻找的高点和低点进行比较。
// I prefer to move these outside the query for clarity.
var raPlus = ra.Value.AddMinute(1);
var raMinus = ra.Value.AddMinute(-1);
var decPlus = dec.Value.AddMinute(1);
var decMinus = dec.Value.AddMinute(-1);
var targets = from item in db.Targets
where item.RightAscension <= raPlus &&
item.RightAscension >= raMinus &&
item.Declination <= decPlus &&
item.Declination >= decMinus
select item;
您已经注意到,自定义函数不能用作查询表达式树的一部分。因此,您要么必须在查询中手动嵌入函数逻辑,从而引入大量代码重复,要么切换到方法语法并使用 return 整个表达式的辅助方法。
后者可以使用System.Linq.Expressions
的方法手动完成,但这并不自然,需要大量知识。让我向您介绍一个更简单的方法。
目标是像这样实现扩展方法
public static IQueryable<T> WhereIsCloseTo<T>(this IQueryable<T> source, Expression<Func<T, double>> comparand, double comparison, double tolerance = EightDecimalPlaces)
{
return source.Where(...);
}
并按如下方式使用
var targets = db.Targets
.WhereIsCloseTo(item => item.RightAscension, ra.Value, WithinOneMinute)
.WhereIsCloseTo(item => item.Declination, dec.Value, WithinOneMinute);
请注意,使用此方法您不能使用 &&
,但链接 Where
会产生相同的结果。
首先,让我们提供与原始函数等效的表达式
public static Expression<Func<double, bool>> IsCloseTo(double comparison, double tolerance = EightDecimalPlaces)
{
return comparand => Math.Abs(comparand - comparison) >= tolerance;
}
问题是不能在我们的方法中直接使用,因为需要Expression<Func<T, bool>>
.
幸运的是,使用我对 :
的回答中的一个小辅助实用程序可以轻松完成
public static class ExpressionUtils
{
public static Expression<Func<TOuter, TResult>> Bind<TOuter, TInner, TResult>(this Expression<Func<TOuter, TInner>> source, Expression<Func<TInner, TResult>> resultSelector)
{
var body = new ParameterExpressionReplacer { source = resultSelector.Parameters[0], target = source.Body }.Visit(resultSelector.Body);
var lambda = Expression.Lambda<Func<TOuter, TResult>>(body, source.Parameters);
return lambda;
}
public static Expression<Func<TOuter, TResult>> ApplyTo<TInner, TResult, TOuter>(this Expression<Func<TInner, TResult>> source, Expression<Func<TOuter, TInner>> innerSelector)
{
return innerSelector.Bind(source);
}
class ParameterExpressionReplacer : ExpressionVisitor
{
public ParameterExpression source;
public Expression target;
protected override Expression VisitParameter(ParameterExpression node)
{
return node == source ? target : base.VisitParameter(node);
}
}
}
现在我们拥有了所需的一切,所以我们的方法实现起来很简单:
public static IQueryable<T> WhereIsCloseTo<T>(this IQueryable<T> source, Expression<Func<T, double>> comparand, double comparison, double tolerance = EightDecimalPlaces)
{
return source.Where(IsCloseTo(comparison, tolerance).ApplyTo(comparand));
}
我最终是这样做的:
const double radius = 1.0;
const double radiusHours = radius / 15.0;
var db = CompositionRoot.GetTargetDatabase();
var ra = rightAscension.Value;
var dec = declination.Value;
var minRa = ra - radiusHours;
var maxRa = ra + radiusHours;
var minDec = dec - radius;
var maxDec = dec + radius;
var closeTargets = from target in db.Targets
where target.RightAscension >= minRa
&& target.RightAscension <= maxRa
&& target.Declination >= minDec
&& target.Declination <= maxDec
let deltaRa = Abs(target.RightAscension - ra) * 15.0 // in degrees
let deltaDec = Abs(target.Declination - dec)
let distanceSquared = deltaRa * deltaRa + deltaDec * deltaDec
orderby distanceSquared
select target;
一个小问题是赤经以小时为单位(每小时 15 度),而赤纬以度为单位,因此我必须在几个地方进行调整。
我首先将列表缩小到 objects 在一个小半径内(在本例中为 1 度)。事实上,我使用的是 1 度平方,但它是一个足够好的近似值。然后我使用 Pythagoras 按距所需点的距离对项目进行排序(我不取平方根,因为这会再次给出错误,但同样足以简单地获得正确的排序)。
最后我具体化查询并取第一个元素作为我的答案。
这仍然不完美,因为它没有处理赤经接近于零的情况。我最终将与负 RA 进行比较,而不是与 23:59 附近的东西进行比较 - 但我现在可以忍受它。
结果为 "announces" 语音合成器提供支持,其中 telescope 指向 object 的名称。很酷:)如果我偶尔错过一个,那也没关系。
我有一个参考数据库,其中包含天体网格上物体的坐标。我想查询数据库并找到给定点 "close to"(在一定 angular 距离内)的对象。
我试过这个查询:
const double WithinOneMinute = 1.0 / 60.0; // 1 minute of arc
var db = CompositionRoot.GetTargetDatabase();
var targets = from item in db.Targets
where item.RightAscension.IsCloseTo(ra.Value, WithinOneMinute)
&& item.Declination.IsCloseTo(dec.Value, WithinOneMinute)
select item;
var found = targets.ToList();
这失败了,因为 LINQ 查询提供程序不理解我的 IsCloseTo
扩展方法,该方法实现为:
public static bool IsCloseTo(this double comparand, double comparison, double tolerance = EightDecimalPlaces)
{
var difference = Math.Abs(comparand - comparison);
return (difference <= tolerance);
}
所以我目前正在寻找想法。有人做过这样的事吗?
LINQ(到 EF)查询提供程序不知道如何在 SQL 中执行您的 IsCloseTo 方法。您需要先枚举您的项目,然后使用您的扩展方法对其进行过滤,如下所示:
var db = CompositionRoot.GetTargetDatabase();
var targets = from item in db.Targets
select item;
//now targets will be enumarated and can be querable with LINQ to objects
var filteredTargets = from target in targets.ToList()
where target.RightAscension.IsCloseTo(ra.Value, WithinOneMinute)
&& target.Declination.IsCloseTo(dec.Value, WithinOneMinute)
select target;
var filteredTargets = targets.ToList();
问题是 Entity Framework 不知道如何将其翻译成 SQL。无需在进入数据库后进行过滤,您可以让查询包含过滤器,就像直接写 SQL 一样。它会更冗长一点,但是拖入大量数据会便宜得多,这些数据一旦内置在内存中就会立即被过滤。
您需要做的是将您的每个时间与您正在寻找的高点和低点进行比较。
// I prefer to move these outside the query for clarity.
var raPlus = ra.Value.AddMinute(1);
var raMinus = ra.Value.AddMinute(-1);
var decPlus = dec.Value.AddMinute(1);
var decMinus = dec.Value.AddMinute(-1);
var targets = from item in db.Targets
where item.RightAscension <= raPlus &&
item.RightAscension >= raMinus &&
item.Declination <= decPlus &&
item.Declination >= decMinus
select item;
您已经注意到,自定义函数不能用作查询表达式树的一部分。因此,您要么必须在查询中手动嵌入函数逻辑,从而引入大量代码重复,要么切换到方法语法并使用 return 整个表达式的辅助方法。
后者可以使用System.Linq.Expressions
的方法手动完成,但这并不自然,需要大量知识。让我向您介绍一个更简单的方法。
目标是像这样实现扩展方法
public static IQueryable<T> WhereIsCloseTo<T>(this IQueryable<T> source, Expression<Func<T, double>> comparand, double comparison, double tolerance = EightDecimalPlaces)
{
return source.Where(...);
}
并按如下方式使用
var targets = db.Targets
.WhereIsCloseTo(item => item.RightAscension, ra.Value, WithinOneMinute)
.WhereIsCloseTo(item => item.Declination, dec.Value, WithinOneMinute);
请注意,使用此方法您不能使用 &&
,但链接 Where
会产生相同的结果。
首先,让我们提供与原始函数等效的表达式
public static Expression<Func<double, bool>> IsCloseTo(double comparison, double tolerance = EightDecimalPlaces)
{
return comparand => Math.Abs(comparand - comparison) >= tolerance;
}
问题是不能在我们的方法中直接使用,因为需要Expression<Func<T, bool>>
.
幸运的是,使用我对
public static class ExpressionUtils
{
public static Expression<Func<TOuter, TResult>> Bind<TOuter, TInner, TResult>(this Expression<Func<TOuter, TInner>> source, Expression<Func<TInner, TResult>> resultSelector)
{
var body = new ParameterExpressionReplacer { source = resultSelector.Parameters[0], target = source.Body }.Visit(resultSelector.Body);
var lambda = Expression.Lambda<Func<TOuter, TResult>>(body, source.Parameters);
return lambda;
}
public static Expression<Func<TOuter, TResult>> ApplyTo<TInner, TResult, TOuter>(this Expression<Func<TInner, TResult>> source, Expression<Func<TOuter, TInner>> innerSelector)
{
return innerSelector.Bind(source);
}
class ParameterExpressionReplacer : ExpressionVisitor
{
public ParameterExpression source;
public Expression target;
protected override Expression VisitParameter(ParameterExpression node)
{
return node == source ? target : base.VisitParameter(node);
}
}
}
现在我们拥有了所需的一切,所以我们的方法实现起来很简单:
public static IQueryable<T> WhereIsCloseTo<T>(this IQueryable<T> source, Expression<Func<T, double>> comparand, double comparison, double tolerance = EightDecimalPlaces)
{
return source.Where(IsCloseTo(comparison, tolerance).ApplyTo(comparand));
}
我最终是这样做的:
const double radius = 1.0;
const double radiusHours = radius / 15.0;
var db = CompositionRoot.GetTargetDatabase();
var ra = rightAscension.Value;
var dec = declination.Value;
var minRa = ra - radiusHours;
var maxRa = ra + radiusHours;
var minDec = dec - radius;
var maxDec = dec + radius;
var closeTargets = from target in db.Targets
where target.RightAscension >= minRa
&& target.RightAscension <= maxRa
&& target.Declination >= minDec
&& target.Declination <= maxDec
let deltaRa = Abs(target.RightAscension - ra) * 15.0 // in degrees
let deltaDec = Abs(target.Declination - dec)
let distanceSquared = deltaRa * deltaRa + deltaDec * deltaDec
orderby distanceSquared
select target;
一个小问题是赤经以小时为单位(每小时 15 度),而赤纬以度为单位,因此我必须在几个地方进行调整。
我首先将列表缩小到 objects 在一个小半径内(在本例中为 1 度)。事实上,我使用的是 1 度平方,但它是一个足够好的近似值。然后我使用 Pythagoras 按距所需点的距离对项目进行排序(我不取平方根,因为这会再次给出错误,但同样足以简单地获得正确的排序)。
最后我具体化查询并取第一个元素作为我的答案。
这仍然不完美,因为它没有处理赤经接近于零的情况。我最终将与负 RA 进行比较,而不是与 23:59 附近的东西进行比较 - 但我现在可以忍受它。
结果为 "announces" 语音合成器提供支持,其中 telescope 指向 object 的名称。很酷:)如果我偶尔错过一个,那也没关系。