如何查询基于双列 "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 的名称。很酷:)如果我偶尔错过一个,那也没关系。