diff --git a/2023/ruby/day_07.rb b/2023/ruby/day_07.rb
new file mode 100644
index 0000000..5894498
--- /dev/null
+++ b/2023/ruby/day_07.rb
@@ -0,0 +1,77 @@
+module PartOne
+ CARD_ORDER = ((?2..?9).to_a + %w[ T J Q K A ]).each.with_index.to_h
+
+ Card = Data.define(:rank) do
+ def <=>(other)
+ CARD_ORDER.fetch(self.rank) <=> CARD_ORDER.fetch(other.rank)
+ end
+ end
+
+ Hand = Data.define(:cards) do
+ def type
+ tally = self.cards.tally
+ case tally.values.sort
+ when [5] then 5 # five of a kind
+ when [1, 4] then 4 # four of a kind
+ when [2, 3] then 3.5 # full house
+ when [1, 1, 3] then 3 # three of a kind
+ when [1, 2, 2] then 2.5 # two pair
+ when [1, 1, 1, 2] then 2 # one pair
+ when [1, 1, 1, 1, 1] then 1 # high card
+ else fail "couldn't calculate type: #{self.inspect}"
+ end
+ end
+
+ def <=>(other)
+ v = self.type <=> other.type
+ v.zero? ? self.cards <=> other.cards : v
+ end
+ end
+end
+
+module PartTwo
+ CARD_ORDER = ([?J] + (?2..?9).to_a + %w[ T Q K A ]).each.with_index.to_h
+
+ Card = Data.define(:rank) do
+ def <=>(other)
+ CARD_ORDER.fetch(self.rank) <=> CARD_ORDER.fetch(other.rank)
+ end
+ end
+
+ Hand = Data.define(:cards) do
+ def type
+ tally = self.cards.map(&:rank).tally
+ jokers = tally.delete(?J) || 0
+ counts = tally.values.sort
+ if counts.empty?
+ counts = [5]
+ else
+ counts[-1] += jokers
+ end
+ case counts
+ when [5] then 5 # five of a kind
+ when [1, 4] then 4 # four of a kind
+ when [2, 3] then 3.5 # full house
+ when [1, 1, 3] then 3 # three of a kind
+ when [1, 2, 2] then 2.5 # two pair
+ when [1, 1, 1, 2] then 2 # one pair
+ when [1, 1, 1, 1, 1] then 1 # high card
+ else fail "couldn't calculate type: #{self.inspect}"
+ end
+ end
+
+ def <=>(other)
+ v = self.type <=> other.type
+ v.zero? ? self.cards <=> other.cards : v
+ end
+ end
+end
+
+hands = ARGF.read.scan(/(\w+)\s+(\d+)/).to_h {|hand, bid|
+ [PartTwo::Hand.new(hand.chars.map { PartTwo::Card.new(_1) }), bid.to_i]
+}
+
+p hands
+ .sort_by(&:first)
+ .map.with_index {|(_,bid),i| bid * (i+1) }
+ .sum