Linq: GroupBy, Sum และ Count


133

ฉันมีชุดผลิตภัณฑ์

public class Product {

   public Product() { }

   public string ProductCode {get; set;}
   public decimal Price {get; set; }
   public string Name {get; set;}
}

ตอนนี้ฉันต้องการจัดกลุ่มคอลเลคชันตามรหัสผลิตภัณฑ์และส่งคืนออบเจ็กต์ที่มีชื่อหมายเลขหรือผลิตภัณฑ์สำหรับแต่ละรหัสและราคารวมสำหรับแต่ละผลิตภัณฑ์

public class ResultLine{

   public ResultLine() { }

   public string ProductName {get; set;}
   public string Price {get; set; }
   public string Quantity {get; set;}
}

ดังนั้นฉันใช้ GroupBy เพื่อจัดกลุ่มตาม ProductCode จากนั้นฉันคำนวณผลรวมและนับจำนวนระเบียนสำหรับแต่ละรหัสผลิตภัณฑ์

นี่คือสิ่งที่ฉันมี:

List<Product> Lines = LoadProducts();    
List<ResultLine> result = Lines
                .GroupBy(l => l.ProductCode)
                .SelectMany(cl => cl.Select(
                    csLine => new ResultLine
                    {
                        ProductName =csLine.Name,
                        Quantity = cl.Count().ToString(),
                        Price = cl.Sum(c => c.Price).ToString(),
                    })).ToList<ResultLine>();

ด้วยเหตุผลบางอย่างผลรวมจะทำอย่างถูกต้อง แต่การนับอยู่เสมอ 1

Sampe data:

List<CartLine> Lines = new List<CartLine>();
            Lines.Add(new CartLine() { ProductCode = "p1", Price = 6.5M, Name = "Product1" });
            Lines.Add(new CartLine() { ProductCode = "p1", Price = 6.5M, Name = "Product1" });
            Lines.Add(new CartLine() { ProductCode = "p2", Price = 12M, Name = "Product2" });

ผลลัพธ์พร้อมข้อมูลตัวอย่าง:

Product1: count 1   - Price:13 (2x6.5)
Product2: count 1   - Price:12 (1x12)

ผลิตภัณฑ์ 1 ควรมีจำนวน = 2!

ฉันพยายามจำลองสถานการณ์นี้ในแอปพลิเคชันคอนโซลอย่างง่าย แต่ฉันได้ผลลัพธ์ดังต่อไปนี้:

Product1: count 2   - Price:13 (2x6.5)
Product1: count 2   - Price:13 (2x6.5)
Product2: count 1   - Price:12 (1x12)

ผลิตภัณฑ์ 1: ควรจะปรากฏเพียงครั้งเดียวเท่านั้น ... รหัสสำหรับด้านบนสามารถพบได้ใน pastebin: http://pastebin.com/cNHTBSie

คำตอบ:


285

ฉันไม่เข้าใจที่ "ผลกับข้อมูลตัวอย่าง" ครั้งแรกที่มาจาก แต่ปัญหาใน app คอนโซลคือการที่คุณกำลังใช้SelectManyจะมองไปที่แต่ละรายการในแต่ละกลุ่ม

ฉันคิดว่าคุณแค่ต้องการ:

List<ResultLine> result = Lines
    .GroupBy(l => l.ProductCode)
    .Select(cl => new ResultLine
            {
                ProductName = cl.First().Name,
                Quantity = cl.Count().ToString(),
                Price = cl.Sum(c => c.Price).ToString(),
            }).ToList();

การใช้First()ที่นี่เพื่อรับชื่อผลิตภัณฑ์ถือว่าทุกผลิตภัณฑ์ที่มีรหัสผลิตภัณฑ์เดียวกันมีชื่อผลิตภัณฑ์เหมือนกัน ดังที่ระบุไว้ในความคิดเห็นคุณสามารถจัดกลุ่มตามชื่อผลิตภัณฑ์เช่นเดียวกับรหัสผลิตภัณฑ์ซึ่งจะให้ผลลัพธ์เดียวกันหากชื่อนั้นเหมือนกันสำหรับรหัสที่กำหนด แต่เห็นได้ชัดว่าสร้าง SQL ที่ดีขึ้นใน EF

ฉันขอแนะนำให้คุณเปลี่ยนQuantityและPriceคุณสมบัติเป็นintและdecimalประเภทตามลำดับ - เหตุใดจึงใช้คุณสมบัติสตริงสำหรับข้อมูลที่ไม่ใช่ข้อความอย่างชัดเจน


ตกลงแอปคอนโซลของฉันทำงาน ขอบคุณสำหรับการชี้ให้ฉันใช้ First () และออก SelectMany ResultLine เป็น ViewModel จริง ๆ ราคาจะได้รับการจัดรูปแบบด้วยสัญลักษณ์สกุลเงิน นั่นเป็นเหตุผลที่ฉันต้องการให้เป็นสตริง แต่ฉันสามารถเปลี่ยนปริมาณเป็น int .. ฉันจะดูตอนนี้ถ้าสิ่งนี้อาจช่วยให้เว็บไซต์ของฉัน ผมจะแจ้งให้คุณทราบ.
ThdK

6
@ThdK: ไม่คุณควรPriceเป็นทศนิยมเช่นกันและเปลี่ยนวิธีการจัดรูปแบบ รักษาความเป็นตัวแทนข้อมูลให้สะอาดและเปลี่ยนเป็นมุมมองการนำเสนอในช่วงเวลาที่เป็นไปได้ล่าสุดเท่านั้น
Jon Skeet

4
ทำไมไม่จัดกลุ่มตาม ProductCode และชื่อ บางสิ่งเช่นนั้น: .GroupBy (l => ใหม่ {l.ProductCode, l.Name}) และใช้ ProductName = c.Key.Name,
Kirill Bestemyanov

@ KirillBestemyanov: ใช่นั่นเป็นอีกทางเลือกหนึ่งอย่างแน่นอน
Jon Skeet

ตกลงมันดูเหมือนว่าคอลเลกชันของฉันเป็นจริงห่อหุ้มรอบคอลเลกชันที่แท้จริง .. ยิงฉัน .. สิ่งที่ดีคือผมมีประสบการณ์บาง Linq วันนี้ :)
ThdK

27

แบบสอบถามต่อไปนี้ใช้งานได้ SelectManyมันใช้แต่ละกลุ่มจะทำเลือกแทน SelectManyทำงานในแต่ละองค์ประกอบจากการรวบรวมแต่ละ ตัวอย่างเช่นในแบบสอบถามของคุณคุณมีผลลัพธ์ของ 2 คอลเลกชัน SelectManyรับผลลัพธ์ทั้งหมดซึ่งรวม 3 แทนที่จะเป็นแต่ละคอลเล็กชัน รหัสต่อไปนี้ใช้ได้กับแต่ละIGroupingส่วนในส่วนที่เลือกเพื่อให้การดำเนินการรวมของคุณทำงานอย่างถูกต้อง

var results = from line in Lines
              group line by line.ProductCode into g
              select new ResultLine {
                ProductName = g.First().Name,
                Price = g.Sum(pc => pc.Price).ToString(),
                Quantity = g.Count().ToString(),
              };

2

บางครั้งคุณจำเป็นต้องเลือกบางฟิลด์โดยFirstOrDefault()หรือsingleOrDefault()คุณสามารถใช้แบบสอบถามด้านล่าง:

List<ResultLine> result = Lines
    .GroupBy(l => l.ProductCode)
    .Select(cl => new Models.ResultLine
            {
                ProductName = cl.select(x=>x.Name).FirstOrDefault(),
                Quantity = cl.Count().ToString(),
                Price = cl.Sum(c => c.Price).ToString(),
            }).ToList();

1
คุณช่วยอธิบายได้ไหมว่าทำไมบางครั้งฉันต้องใช้FirstOrDefault() or singleOrDefault () `
Shanteshwar Inde

@ShanteshwarInde First () และ FirstOrDefault () รับวัตถุแรกในซีรีส์ในขณะที่ Single () และ SingleOrDefault () คาดหวังเพียง 1 จากผลลัพธ์ หาก Single () และ SingleOrDefault () เห็นว่ามีวัตถุมากกว่า 1 รายการในชุดผลลัพธ์หรือเป็นผลมาจากอาร์กิวเมนต์ที่ระบุไว้มันจะส่งข้อยกเว้น ในการใช้งานคุณใช้ตัวแรกเมื่อคุณต้องการอาจจะเป็นตัวอย่างของซีรีส์และวัตถุอื่น ๆ ที่ไม่สำคัญกับคุณในขณะที่คุณใช้อันหลังหากคุณคาดหวังเพียงวัตถุเดียวและทำบางสิ่งถ้ามีผลลัพธ์มากกว่าหนึ่งรายการ เช่นบันทึกข้อผิดพลาด
Kristianne Nerona
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.