diff --git a/2016/ruby/Gemfile b/2016/ruby/Gemfile
new file mode 100644
index 0000000..2b8ce04
--- /dev/null
+++ b/2016/ruby/Gemfile
@@ -0,0 +1,3 @@
+source 'https://rubygems.org'
+
+gem 'minitest'
diff --git a/2016/ruby/Gemfile.lock b/2016/ruby/Gemfile.lock
new file mode 100644
index 0000000..75b11ae
--- /dev/null
+++ b/2016/ruby/Gemfile.lock
@@ -0,0 +1,13 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ minitest (5.10.0)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ minitest
+
+BUNDLED WITH
+ 1.13.6
diff --git a/2016/ruby/day_01.rb b/2016/ruby/day_01.rb
new file mode 100644
index 0000000..cf606b2
--- /dev/null
+++ b/2016/ruby/day_01.rb
@@ -0,0 +1,67 @@
+include Math
+
+def follow_instructions(input)
+ seq = input.split(', ')
+
+ # start facing north at the origin
+ dir = -PI/2
+ locations = [[0,0]]
+ seq.each.with_object(locations) do |step, locations|
+ /(?<turn>\w)(?<blocks>\d+)/ =~ step
+
+ case turn
+ when ?R
+ dir += PI/2
+ when ?L
+ dir -= PI/2
+ else
+ raise "unexpected turn: #{turn}"
+ end
+
+ blocks.to_i.times do
+ location = locations.last
+ x = location[0] + cos(dir).to_i
+ y = location[1] + sin(dir).to_i
+ locations << [x, y]
+ end
+ end
+
+ locations
+end
+
+def first_dupe(ary)
+ seen = Set.new
+ ary.find { |elem|
+ seen.include?(elem) || (seen << elem && false)
+ }
+end
+
+require 'minitest'
+# require 'minitest/autorun'
+class TestInstructions < Minitest::Test
+ def test_instructions
+ assert_distance 5, 'R2, L3'
+ assert_distance 2, 'R2, R2, R2'
+ assert_distance 12, 'R5, L5, R5, R3'
+ end
+
+ def test_first_dupe
+ assert_equal 1, first_dupe([1, 2, 3, 1])
+ assert_equal 2, first_dupe([1, 2, 3, 2])
+ assert_equal 3, first_dupe([1, 2, 3, 3])
+
+ locations = follow_instructions('R8, R4, R4, R8')
+ assert_equal [4, 0], first_dupe(locations)
+ end
+
+ def assert_distance(expected, instructions)
+ locations = follow_instructions(instructions)
+ assert_equal expected, locations.last.map(&:abs).inject(:+)
+ end
+end
+
+if __FILE__ == $0
+ puts first_dupe(follow_instructions(DATA.read))
+end
+
+__END__