Jeśli szukasz „dokładnej rzeczy”, jak wspomniany post, dotyczący .NET, prawdopodobnie tak naprawdę nie zostanie zaimplementowany w ten sposób. Możesz to zrobić, ale prawdopodobnie nie będziesz zawracać sobie głowy i faktycznie wybierz jedną z innych alternatyw, chyba że potrzebujesz „elastycznych interwałów” w takim stopniu, w jakim ja to robię.
Agregacja płynna
Jeśli masz dostępny nowoczesny serwer MongoDB 3.6 lub nowszy, możesz użyć $dateFromParts
w celu zrekonstruowania daty z "zaokrąglonych" części wyodrębnionych z daty:
DateTime startDate = new DateTime(2018, 5, 1, 0, 0, 0, DateTimeKind.Utc);
DateTime endDate = new DateTime(2018, 6, 1, 0, 0, 0, DateTimeKind.Utc);
var result = Collection.Aggregate()
.Match(k => k.Timestamp >= startDate && k.Timestamp < endDate)
.Group(k =>
new DateTime(k.Timestamp.Year, k.Timestamp.Month, k.Timestamp.Day,
k.Timestamp.Hour, k.Timestamp.Minute - (k.Timestamp.Minute % 15), 0),
g => new { _id = g.Key, count = g.Count() }
)
.SortBy(d => d._id)
.ToList();
Oświadczenie wysłane do serwera:
[
{ "$match" : {
"Timestamp" : {
"$gte" : ISODate("2018-05-01T00:00:00Z"),
"$lt" : ISODate("2018-06-01T00:00:00Z")
}
} },
{ "$group" : {
"_id" : {
"$dateFromParts" : {
"year" : { "$year" : "$Timestamp" },
"month" : { "$month" : "$Timestamp" },
"day" : { "$dayOfMonth" : "$Timestamp" },
"hour" : { "$hour" : "$Timestamp" },
"minute" : { "$subtract" : [
{ "$minute" : "$Timestamp" },
{ "$mod" : [ { "$minute" : "$Timestamp" }, 15 ] }
] },
"second" : 0
}
},
"count" : { "$sum" : 1 }
} },
{ "$sort": { "_id": 1 } }
]
Jeśli nie masz dostępnej tej funkcji, możesz po prostu to pominąć i pozostawić datę „rozmontowaną”, ale następnie złożyć ją ponownie podczas przetwarzania kursora. Wystarczy symulować za pomocą listy:
var result = Collection.Aggregate()
.Match(k => k.Timestamp >= startDate && k.Timestamp < endDate)
.Group(k => new
{
year = k.Timestamp.Year,
month = k.Timestamp.Month,
day = k.Timestamp.Day,
hour = k.Timestamp.Hour,
minute = k.Timestamp.Minute - (k.Timestamp.Minute % 15)
},
g => new { _id = g.Key, count = g.Count() }
)
.SortBy(d => d._id)
.ToList();
foreach (var doc in result)
{
//System.Console.WriteLine(doc.ToBsonDocument());
System.Console.WriteLine(
new BsonDocument {
{ "_id", new DateTime(doc._id.year, doc._id.month, doc._id.day,
doc._id.hour, doc._id.minute, 0) },
{ "count", doc.count }
}
);
}
Oświadczenie wysłane do serwera:
[
{ "$match" : {
"Timestamp" : {
"$gte" : ISODate("2018-05-01T00:00:00Z"),
"$lt" : ISODate("2018-06-01T00:00:00Z")
}
} },
{ "$group" : {
"_id" : {
"year" : { "$year" : "$Timestamp" },
"month" : { "$month" : "$Timestamp" },
"day" : { "$dayOfMonth" : "$Timestamp" },
"hour" : { "$hour" : "$Timestamp" },
"minute" : { "$subtract" : [
{ "$minute" : "$Timestamp" },
{ "$mod" : [ { "$minute" : "$Timestamp" }, 15 ] }
] }
},
"count" : { "$sum" : 1 }
} },
{ "$sort" : { "_id" : 1 } }
]
Pod względem kodu jest między nimi bardzo niewielka różnica. Tyle, że w jednym przypadku „odrzucenie” do DateTime
faktycznie dzieje się na serwerze z $dateFromParts
a w drugim po prostu wykonujemy dokładnie to samo rzutowanie za pomocą DateTime
konstruktora w kodzie podczas iteracji każdego wyniku kursora.
Tak więc w rzeczywistości są one prawie takie same, a jedyną prawdziwą różnicą jest to, że „serwer” wykonuje rzutowanie zwróconej daty, zużywa o wiele mniej bajtów na dokument. W rzeczywistości "5 razy" mniej, ponieważ wszystkie tutaj formaty numeryczne (w tym data BSON) są oparte na 64-bitowych liczbach całkowitych. Mimo to wszystkie te liczby są nadal „lżejsze” niż odesłanie jakiejkolwiek „łańcuchowej” reprezentacji daty.
Kwerendy LINQ
Oto podstawowe formy, które naprawdę pozostają takie same podczas mapowania na te różne formy:
var query = from p in Collection.AsQueryable()
where p.Timestamp >= startDate && p.Timestamp < endDate
group p by new DateTime(p.Timestamp.Year, p.Timestamp.Month, p.Timestamp.Day,
p.Timestamp.Hour, p.Timestamp.Minute - (p.Timestamp.Minute % 15), 0) into g
orderby g.Key
select new { _id = g.Key, count = g.Count() };
Oświadczenie wysłane do serwera:
[
{ "$match" : {
"Timestamp" : {
"$gte" : ISODate("2018-05-01T00:00:00Z"),
"$lt" : ISODate("2018-06-01T00:00:00Z")
}
} },
{ "$group" : {
"_id" : {
"$dateFromParts" : {
"year" : { "$year" : "$Timestamp" },
"month" : { "$month" : "$Timestamp" },
"day" : { "$dayOfMonth" : "$Timestamp" },
"hour" : { "$hour" : "$Timestamp" },
"minute" : { "$subtract" : [
{ "$minute" : "$Timestamp" },
{ "$mod" : [ { "$minute" : "$Timestamp" }, 15 ] }
] },
"second" : 0
}
},
"__agg0" : { "$sum" : 1 }
} },
{ "$sort" : { "_id" : 1 } },
{ "$project" : { "_id" : "$_id", "count" : "$__agg0" } }
]
Lub używając GroupBy()
var query = Collection.AsQueryable()
.Where(k => k.Timestamp >= startDate && k.Timestamp < endDate)
.GroupBy(k =>
new DateTime(k.Timestamp.Year, k.Timestamp.Month, k.Timestamp.Day,
k.Timestamp.Hour, k.Timestamp.Minute - (k.Timestamp.Minute % 15), 0),
(k, s) => new { _id = k, count = s.Count() }
)
.OrderBy(k => k._id);
Oświadczenie wysłane do serwera:
[
{ "$match" : {
"Timestamp" : {
"$gte" : ISODate("2018-05-01T00:00:00Z"),
"$lt" : ISODate("2018-06-01T00:00:00Z")
}
} },
{ "$group" : {
"_id" : {
"$dateFromParts" : {
"year" : { "$year" : "$Timestamp" },
"month" : { "$month" : "$Timestamp" },
"day" : { "$dayOfMonth" : "$Timestamp" },
"hour" : { "$hour" : "$Timestamp" },
"minute" : { "$subtract" : [
{ "$minute" : "$Timestamp" },
{ "$mod" : [ { "$minute" : "$Timestamp" }, 15 ] }
] },
"second" : 0
}
},
"count" : { "$sum" : 1 }
} },
{ "$sort" : { "_id" : 1 } }
]
Jak widać, to w zasadzie ta sama forma
Konwersja oryginału
Jeśli chcesz odtworzyć oryginalny formularz „daty matematycznej” w formie opublikowanej, obecnie wykracza to poza zakres tego, co faktycznie możesz zrobić za pomocą konstruktorów LINQ lub Fluent. Jedynym sposobem na uzyskanie tej samej sekwencji jest użycie BsonDocument
budowa:
DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var group = new BsonDocument { {
"$group",
new BsonDocument {
{ "_id",
new BsonDocument { {
"$add", new BsonArray
{
new BsonDocument { {
"$subtract",
new BsonArray {
new BsonDocument { { "$subtract", new BsonArray { "$Timestamp", epoch } } },
new BsonDocument { {
"$mod", new BsonArray
{
new BsonDocument { { "$subtract", new BsonArray { "$Timestamp", epoch } } },
1000 * 60 * 15
}
} }
}
} },
epoch
}
} }
},
{
"count", new BsonDocument("$sum", 1)
}
}
} };
var query = sales.Aggregate()
.Match(k => k.Timestamp >= startDate && k.Timestamp < endDate)
.AppendStage<BsonDocument>(group)
.Sort(new BsonDocument("_id", 1))
.ToList();
Żądanie wysłane do serwera:
[
{ "$match" : {
"Timestamp" : {
"$gte" : ISODate("2018-05-01T00:00:00Z"),
"$lt" : ISODate("2018-06-01T00:00:00Z")
}
} },
{ "$group" : {
"_id" : {
"$add" : [
{ "$subtract" : [
{ "$subtract" : [ "$Timestamp", ISODate("1970-01-01T00:00:00Z") ] },
{ "$mod" : [
{ "$subtract" : [ "$Timestamp", ISODate("1970-01-01T00:00:00Z") ] },
900000
] }
] },
ISODate("1970-01-01T00:00:00Z")
]
},
"count" : { "$sum" : 1 }
} },
{ "$sort" : { "_id" : 1 } }
]
Głównym powodem, dla którego nie możemy tego teraz zrobić, jest to, że bieżąca serializacja instrukcji zasadniczo nie zgadza się z punktem, w którym .NET Framework mówi, że odejmując dwa DateTime
wartości zwracają TimeSpan
, a konstrukcja MongoDB polegająca na odjęciu dwóch dat BSON zwraca „milisekundy od epoki”, czyli zasadniczo tak działa matematyka.
„Dosłowne” tłumaczenie wyrażenia lambba to zasadniczo:
p => epoch.AddMilliseconds(
(p.Timestamp - epoch).TotalMilliseconds
- ((p.Timestamp - epoch).TotalMilliseconds % 1000 * 60 * 15))
Ale mapowanie nadal wymaga trochę pracy, aby albo rozpoznać oświadczenia, albo sformalizować, jakiego rodzaju oświadczenia są faktycznie przeznaczone do tego celu.
W szczególności MongoDB 4.0 wprowadza $convert
operator i popularne aliasy $toLong
i $toDate
, które mogą być używane w potoku zamiast bieżącej obsługi „dodawania” i „odejmowania” z datami BSON. Zaczynają one tworzyć bardziej „formalną” specyfikację dla takich konwersji, a nie pokazaną metodę, która opiera się wyłącznie na „dodawaniu” i „odejmowaniu”, co jest nadal ważne, ale takie nazwane operatory są znacznie jaśniejsze w kodzie:
{ "$group": {
"_id": {
"$toDate": {
"$subtract": [
{ "$toLong": "$Timestamp" },
{ "$mod": [{ "$toLong": "$Timestamp" }, 1000 * 60 * 15 ] }
]
}
},
"count": { "$sum": 1 }
}}
Łatwo zauważyć, że przy „sformalizowanych” operatorach do konstruowania instrukcji z LINQ dla takich funkcji „DateToLong” i „LongToDate”, instrukcja staje się znacznie czystsza bez typów „koercji” pokazanych w „niedziałającym” wyrażeniu lambda gotowe.