如何基于双列“接近”某个值来查询数据集
本文关键字:查询 数据集 何基于 接近 | 更新日期: 2023-09-27 18:34:43
我有一个参考数据库,用于保存天体网格上物体的坐标。我想查询数据库并查找"接近"(在给定点的某个角距离内(的对象。
我试过这个查询:
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);
}
所以我目前被困在想法上。有人做过这样的事情吗?
正如您已经注意到的,自定义函数不能用作查询表达式树的一部分。因此,您要么必须手动将函数逻辑嵌入到查询中,从而引入大量代码重复,要么切换到方法语法并使用返回整个表达式的帮助程序方法。
后者可以使用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>>
.
通过使用我的答案中的一个小助手实用程序轻松完成,以将表达式的一部分定义为 c# 中的变量:
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));
}
问题是实体框架不知道如何将其转换为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;
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();
这就是我最终做到的:
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 度(,而赤纬是以度为单位,所以我必须在几个地方进行调整。
我首先将列表缩小到小半径(在本例中为 1 度(内的物体。事实上,我使用的是 1 度正方形,但这是一个足够好的近似值。然后,我使用毕达哥拉斯按距所需点的距离对项目进行排序(我不取平方根,因为这会再次产生错误,但同样,只需获得正确的排序就足够了(。
最后,我将查询具体化,并将第一个元素作为我的答案。
这仍然不完美,因为它不能处理右升接近零的情况。我最终会与负 RA 进行比较,而不是接近 23:59 的东西 - 但我现在可以忍受它。
结果为语音合成器提供动力,该语音合成器"宣布"望远镜指向的位置作为物体的名称。很酷的:)如果我偶尔错过一个,那也没关系。