# ############################################################################################## # # Copyright (c) Microsoft Corporation. # # This source code is subject to terms and conditions of the Apache License, Version 2.0. A # copy of the license can be found in the License.html file at the root of this distribution. If # you cannot locate the Apache License, Version 2.0, please send an email to # ironruby@microsoft.com. By using this source code in any fashion, you are agreeing to be bound # by the terms of the Apache License, Version 2.0. # # You must not remove this notice, or any other, from this software. # # ############################################################################################## # 101 LINQ Samples in Ruby (see http://msdn.microsoft.com/en-us/vcsharp/aa336746.aspx) load_assembly 'System.Core' using_clr_extensions System::Linq # Linq Helpers class Object def to_seq(type = Object) System::Linq::Enumerable.method(:of_type).of(type).call(self.to_a) end end make_pair = lambda { |a,b| [a,b] } identity = lambda { |a| a } ############ ### Data ### ############ Product = Struct.new(:product_name, :category, :units_in_stock, :unit_price) products = [ Product["product 1", "foo", 4, 1.3], Product["product 2", "bar", 3, 10.0], Product["product 3", "baz", 0, 4.0], Product["product 4", "foo", 1, 2.5], ] Order = Struct.new(:id, :total, :order_date) orders = [ Order[0, 56.4, 1995], Order[1, 100.3, 2001], Order[2, 1000.0, 1992], Order[3, 1100.4, 2005], Order[4, 150.3, 2004], Order[5, 1040.0, 1996], ] Customer = Struct.new(:id, :customer_name, :company_name, :region, :orders) customers = [ Customer[0, "customer 1", "company 1", "WA", [orders[0], orders[1], orders[5]]], Customer[1, "customer 2", "company 2", "CA", [orders[2], orders[3]]], Customer[2, "customer 3", "company 3", "NY", [orders[4]]], Customer[3, "customer 4", "company 4", "WA", []], ] products = products.to_seq orders = orders.to_seq customers = customers.to_seq numbers = [ 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 ].to_seq digits_array = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"] digits = digits_array.to_seq ################### ### Restriction ### ################### puts '-- Where: Simple 1' numbers.where(lambda { |n| n < 5 }).each { |x| puts x } puts '-- Where: Simple 2' products.where(lambda { |p| p.units_in_stock == 0 }).each { |x| puts x.product_name } puts '-- Where: Simple 3' products.where(lambda { |p| p.units_in_stock > 0 and p.unit_price > 3.00 }).each { |x| puts x.product_name } puts '-- Where: Drilldown' wa_customers = customers.where(lambda {|c| c.region == "WA"}) wa_customers.each do |customer| puts "Customer #{customer.id}: #{customer.company_name}" customer.orders.each do |order| puts " Order #{order.id}: #{order.order_date}" end end puts '-- Where: Indexed' digits.where(lambda { |digit, index| digit.size < index }).each { |x| puts x } ################## ### Projection ### ################## puts '-- Select: Simple 1' p numbers.select(lambda { |n| n + 1 }).to_a puts '-- Select: Simple 2' p products.select(lambda { |p| p.product_name }).to_a puts '-- Select: Transformation' p numbers.select(lambda { |n| digits_array[n] }).to_a puts '-- Select: Anonymous Types 1' words = [ "aPPLE", "BlUeBeRrY", "cHeRry" ].to_seq s = Struct.new(:upper, :lower) words. select(lambda { |w| s[w.upcase, w.downcase] }). each { |ul| puts "Uppercase: #{ul.upper} Lowercase: #{ul.lower}" } puts '-- Select: Anonymous Types 2' s = Struct.new(:digit, :even) numbers. select(lambda {|n| s[digits_array[n], n % 2 == 0] }). each { |entry| p entry } puts '-- Select: Anonymous Types 3' s = Struct.new(:product_name, :category, :price) products. select(lambda {|p| s[p.product_name, p.category, p.unit_price] }). each { |product_info| puts "#{product_info.product_name} is in the category #{product_info.category} and costs #{product_info.price} per unit" } puts '-- Select: Indexed' numbers.select(lambda { |num, index| [num, num == index] }).each { |num, in_place| puts "#{num}: #{in_place}" } puts '-- Select: Filtered' p numbers.where(lambda { |n| n < 5 }).select(lambda { |n| digits_array[n] }).to_a puts '-- SelectMany: Compound from 1' numbersA = [ 0, 2, 4, 5, 6, 8, 9 ].to_seq numbersB = [ 1, 3, 5, 7, 8 ].to_seq numbersA. select_many(lambda { |_| numbersB }, make_pair). where(lambda { |(a,b)| a < b }). each { |a,b| puts "#{a} is less than #{b}" } puts '-- SelectMany: Compound from 2' s = Struct.new(:customer_id, :order_id, :total) customers. select_many(lambda { |c| c.orders }, make_pair). where(lambda { |(c,o)| o.total < 500 }). select(lambda { |(c,o)| s[c.id, o.id, o.total] }). each { |entry| p entry } #puts '-- SelectMany: Compound from 3' #puts '-- SelectMany: from Assignment' puts '-- SelectMany: Multiple from' cutoff_date = 1997 s = Struct.new(:customer_id, :order_id) customers. where(lambda { |c| c.region == "WA" }). select_many(lambda { |c| c.orders }, make_pair). where(lambda { |(c, o)| o.order_date >= cutoff_date }). select(lambda { |(c, o)| s[c.id, o.id] }). each { |entry| p entry } puts '-- SelectMany: Indexed' customers. select_many(lambda { |cust, cust_index| cust.orders.to_seq.select(lambda {|o| "Customer #{cust_index + 1} has an order with id #{o.id}" }) }). each { |s| puts s } ##################### ### Parititioning ### ##################### puts '-- Take: Simple' p numbers.take(3).to_a #puts '-- Take: Nested' puts '-- Skip: Simple' p numbers.skip(3).to_a puts '-- Skip: Nested' s = Struct.new(:customer_id, :order_id, :order_date) customers. select_many(lambda { |c| c.orders }, make_pair). where(lambda {|(c,o)| c.region == "WA" }). select(lambda {|(c,o)| s[c.id, o.id, o.order_date]}). skip(2). each { |entry| p entry } puts '-- TakeWhile: Simple' p numbers.take_while(lambda {|n| n < 6 }).to_a puts '-- TakeWhile: Indexed' p numbers.take_while(lambda {|n, index| n >= index }).to_a puts '-- SkipWhile: Simple' p numbers.skip_while(lambda {|n| n % 3 != 0 }).to_a puts '-- SkipWhile: Indexed' p numbers.skip_while(lambda {|n, index| n >= index }).to_a ################ ### Ordering ### ################ class Comparer include System::Collections::Generic::IComparer[String] def initialize &comparer @comparer = comparer end def compare(x,y) @comparer[x,y] end end # puts '-- OrderBy - Simple 1' # puts '-- OrderBy - Simple 2' # puts '-- OrderBy - Simple 3' # puts '-- OrderBy - Comparer' # puts '-- OrderByDescending - Simple 1' # puts '-- OrderByDescending - Simple 2' # puts '-- OrderByDescending - Comparer' # puts '-- ThenBy - Simple' # puts '-- ThenBy - Comparer' puts '-- ThenByDescending: Simple' products.order_by(lambda { |p| p.category }).then_by_descending(lambda { |p| p.unit_price }).each { |product| p product } puts '-- ThenByDescending: Comparer' words = [ "aPPLE", "AbAcUs", "bRaNcH", "BlUeBeRrY", "ClOvEr", "cHeRry" ].to_seq p words.order_by(lambda { |a| a.size }).then_by_descending(identity, Comparer.new { |x, y| x.casecmp y }).to_a puts '-- Reverse' digits.where(lambda { |d| d[1] == ?i }).reverse.each { |d| puts d } ################ ### Grouping ### ################ puts '-- Grouping: Simple 1' numbers. group_by(lambda { |n| n % 5 }). each { |g| puts "Numbers with a remainder of #{g.key} when divided by 5: #{g.to_a.inspect}" } puts '-- Grouping: Simple 2' words = [ "blueberry", "chimpanzee", "abacus", "banana", "apple", "cheese" ].to_seq words. group_by(lambda { |w| w[0] }). each { |g| puts "Words that start with the letter '#{g.key.chr}': #{g.to_a.join(', ')}" } #puts '-- Grouping: Simple 3' #puts '-- GroupBy: Nested' puts '-- GroupBy: Comparer' class AnagramEqualityComparer include System::Collections::Generic::IEqualityComparer[String] def equals(x,y) get_canoncial_string(x) == get_canoncial_string(y) end def get_hash_code(x) get_canoncial_string(x).GetHashCode end private def get_canoncial_string(word) word.split('').sort.join end end anagrams = ["from ", " salt", " earn ", " last ", " near ", " form "].to_seq anagrams. group_by(lambda { |w| w.strip }, AnagramEqualityComparer.new). each { |g| puts "#{g.key}: #{g.select(lambda { |w| "'#{w}'" }).to_a.join(', ')}" } puts '-- GroupBy: Comparer, Mapped' anagrams. group_by(lambda { |w| w.strip }, lambda { |a| a.upcase }, AnagramEqualityComparer.new). each { |g| puts "#{g.key}: #{g.select(lambda { |w| "'#{w}'" }).to_a.join(', ')}" } ##################### ### Set Operators ### ##################### puts '-- Distinct 1' p [ 2, 2, 3, 5, 5 ].to_seq.distinct.to_a #puts '-- Distinct 2' puts '-- Union 1' p numbersA.union(numbersB).to_a #puts '-- Union 2' puts '-- Intersect 1' p numbersA.intersect(numbersB).to_a #puts '-- Intersect 2' puts '-- Except 1' p numbersA.except(numbersB).to_a #puts '-- Except 2' ############################ ### Conversion Operators ### ############################ puts '-- ToArray' doubles = [1.7, 2.3, 1.9, 4.1, 2.9].to_seq p doubles.order_by(identity).to_array puts '-- ToList' words = ["cherry", "apple", "blueberry"].to_seq p words.order_by(identity).to_list puts '-- ToDictionary' s = Struct.new(:name, :score) score_records = [s["Alice", 50], s["Bob", 40], s["Cathy", 45]].to_seq dict = score_records.to_dictionary(lambda {|sr| sr.name }) dict.each {|k,v| puts "#{k} => #{v}" } # TODO: IronRuby bug #puts '-- OfType' #p [ nil, 1.0, "two", 3, "four", 5, "six", 7.0 ].to_seq.method(:of_type).of(Float).call.to_a ######################### ### Element Operators ### ######################### puts '-- First: Simple' p products.where(lambda {|p| p.category == "foo" }).first puts '-- First: Condition' p digits.first(lambda {|s| s[0] == ?o}) puts '-- FirstOrDefault: Simple' p [].to_seq(Fixnum).first_or_default puts '-- FirstOrDefault: Condition' p digits.first_or_default(lambda {|s| s[0] == ?x}).nil? puts '-- ElementAt' p numbers.where(lambda {|n| n > 5}).element_at(1) ############################ ### Generation Operators ### ############################ puts '-- Range' System::Linq::Enumerable.range(10, 5).each { |n| puts "#{n} is #{n % 2 == 1 ? 'odd' : 'even'}" } puts '-- Repeat' System::Linq::Enumerable.repeat(7, 10).each { |n| print n } ################### ### Quantifiers ### ################### puts '-- Any: Simple' words = ["believe", "relief", "receipt", "field"].to_seq p words.any(lambda { |w| w.include? "ei" }) puts '-- Any: Grouped' products. group_by(lambda { |p| p.category }). where(lambda { |g| g.any(lambda { |p| p.units_in_stock == 0 }) }). each { |g| puts "#{g.key}: #{g.select(lambda {|p| p.product_name }).to_a.join(', ')}" } puts '-- All: Simple' p numbers.all(lambda { |n| n % 2 == 1 }) puts '-- All: Grouped' products. group_by(lambda { |p| p.category }). where(lambda { |g| g.all(lambda { |p| p.units_in_stock > 0 }) }). each { |g| puts "#{g.key}: #{g.select(lambda {|p| p.product_name }).to_a.join(', ')}" } ########################### ### Aggregate Operators ### ########################### puts '-- Count: Simple' factorsOf300 = [ 2, 2, 3, 5, 5 ].to_seq p factorsOf300.distinct.count puts '-- Count: Conditional' p numbers.count(lambda { |n| n % 2 == 1 }) puts '-- Count: Nested' p customers.select(lambda {|c| [c.id, c.orders.to_seq.count]}).to_a puts '-- Count: Grouped' s = Struct.new(:category, :product_count) products. group_by(lambda { |p| p.category }, lambda { |c, g| s[c, g.count ] }). each { |entry| p entry } puts '-- Sum: Simple' # sum is only defined on IEnumerable, but numbers are of type IEnumerable: p numbers.to_seq(Fixnum).sum puts '-- Sum: Projection' # sum is overloaded on the Func's return type, which we don't currently infer, so we need it to be specified explicitly: p words.sum(System::Func[Object, Fixnum].new { |w| w.length }) puts '-- Sum: Grouped' s = Struct.new(:category, :total_units_in_stock) products. group_by(lambda { |p| p.category }, lambda { |c, g| s[c, g.sum(System::Func[Object, Fixnum].new { |p| p.units_in_stock })] }). each { |entry| p entry } puts '-- Min: Simple' # min is only defined on IEnumerable, but numbers are of type IEnumerable: p numbers.to_seq(Fixnum).min puts '-- Min: Projection' # min is overloaded on the Func's return type, which we don't currently infer, so we need it to be specified explicitly: p words.sum(System::Func[Object, Fixnum].new { |w| w.length }) puts '-- Min: Grouped' s = Struct.new(:category, :cheapest_price) products. group_by(lambda { |p| p.category }, lambda do |c, g| # min has overloads that we are currently not able to disambiguate via type inference, so we need some help: s[c, g.method(:min).of(Object, Float).call(lambda { |p| p.unit_price })] end). each { |entry| p entry } puts '-- Min: Elements' s = Struct.new(:category, :cheapest_products) products. group_by(lambda { |p| p.category }, lambda do |c, g| # min has overloads that we are currently not able to disambiguate via type inference, so we need some help: min_price = g.method(:min).of(Object, Float).call(lambda { |p| p.unit_price }) s[c, g.where(lambda {|p| p.unit_price == min_price })] end). each { |entry| puts "Cheapest products in category #{entry.category}: #{entry.cheapest_products.select(lambda {|p| p.product_name }).to_a}" } # Max samples are similar to Min # Average samples are similar to Sum puts '-- Aggregate: Simple' doubles = [ 1.7, 2.3, 1.9, 4.1, 2.9 ].to_seq p doubles.aggregate(lambda { |running_product, next_factor| running_product * next_factor }) puts '-- Aggregate: Seed' start_balance = 100.0 attempted_withdrawals = [ 20, 10, 40, 50, 10, 70, 30 ].to_seq end_balance = attempted_withdrawals.aggregate start_balance, lambda { |balance, next_withdrawal| next_withdrawal <= balance ? balance - next_withdrawal : balance } p end_balance ############################### ### Miscellaneous Operators ### ############################### puts '-- Concat: 1' p numbersA.concat(numbersB).to_a puts '-- Concat: 2' customer_names = customers.select(lambda {|c| c.company_name }) product_names = products.select(lambda {|p| p.product_name }) p customer_names.concat(product_names).to_a puts '-- EqualAll: 1' wordsA = ["cherry", "apple", "blueberry"].to_seq wordsB = ["cherry", "apple", "blueberry"].to_seq p wordsA.sequence_equal(wordsB) puts '-- EqualAll: 2' wordsA = ["cherry", "apple", "blueberry"].to_seq wordsB = [ "apple", "blueberry", "cherry" ].to_seq p wordsA.sequence_equal(wordsB) ################################# ### Custom Sequence Operators ### ################################# # N/A ####################### ### Query Execution ### ####################### # ... ###################### ### Join Operators ### ###################### puts '-- Cross Join' categories = ["bar", "foo"].to_seq categories. join(products, identity, lambda { |p| p.category }, make_pair). each { |(c, p)| puts "#{p.product_name}: #{c}" } puts '-- Group Join' categories. group_join(products, identity, lambda { |p| p.category }, make_pair). each { |(c, ps)| puts "#{c}: #{ps.select(lambda {|p| p.product_name}).to_a.join(', ')}" } puts '-- Cross Join with Group Join' s = Struct.new(:category, :product_name) categories. group_join(products, identity, lambda { |p| p.category }, make_pair). select_many(lambda { |(c, ps)| ps }, lambda { |(c,), p| s[c, p.product_name] }). each { |entry| p entry} puts '-- Left Outer Join' categories = ["foo", "no such category"].to_seq categories. group_join(products, identity, lambda { |p| p.category }, make_pair). select_many(lambda { |(c, ps)| ps.default_if_empty }, lambda { |(c,), p| s[c, if p.nil? then "(No Products)" else p.product_name end] }). each { |entry| p entry}