Permalink
Browse files

Ruby implementation of two phase sets.

  • Loading branch information...
1 parent 70c9553 commit cd835afe829dddd3322711622966056c366d59e2 @aphyr committed Jan 6, 2012
Showing with 421 additions and 0 deletions.
  1. +20 −0 lib/meangirls.rb
  2. +10 −0 lib/meangirls/crdt.rb
  3. +61 −0 lib/meangirls/set.rb
  4. +95 −0 lib/meangirls/two_phase_set.rb
  5. +16 −0 spec/crdt.rb
  6. +6 −0 spec/init.rb
  7. +21 −0 spec/run
  8. +168 −0 spec/set.rb
  9. +24 −0 spec/two_phase_set.rb
View
@@ -0,0 +1,20 @@
+module Meangirls
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__))
+
+ class ReinsertNotAllowed < RuntimeError; end
+ class DeleteNotAllowed < RuntimeError; end
+
+ require 'set'
+ require 'meangirls/crdt'
+
+ # Transforms a JSON data structure into a CRDT datatype.
+ def parse(s)
+ case s['type']
+ when '2p-set'
+ TwoPhaseSet.new s
+ else
+ raise ArgumentError, "unknown type #{s['type']}"
+ end
+ end
+ module_function :parse
+end
View
@@ -0,0 +1,10 @@
+class Meangirls::CRDT
+ require 'meangirls/set'
+
+ # Merge a list of CRDTs by folding over merge.
+ def self.merge(*os)
+ [*os].inject do |o1, o2|
+ o1.merge o2
+ end
+ end
+end
View
@@ -0,0 +1,61 @@
+class Meangirls::Set < Meangirls::CRDT
+ require 'meangirls/two_phase_set'
+
+ include Enumerable
+
+ # Add all elements of other to a copy of self.
+ def |(other)
+ other.inject(clone) do |copy, e|
+ copy << e
+ end
+ end
+
+ # Return a copy of self where [all elements not present in other] have been
+ # deleted.
+ def &(other)
+ (to_set - other.to_set).inject(clone) do |copy, e|
+ copy.delete e
+ copy
+ end
+ end
+
+ # Add all elements of other to a copy of self.
+ def +(other)
+ other.inject(clone) do |copy, e|
+ copy << e
+ end
+ end
+
+ # Remove all elements of other from a copy of self.
+ def -(other)
+ other.inject(clone) do |copy, e|
+ copy.delete e
+ copy
+ end
+ end
+
+ # Loose equality: present elements of each set are equal
+ def ===(other)
+ to_set == other.to_set
+ end
+
+ # Iterate over all present elements
+ def each(&block)
+ to_set.each(&block)
+ end
+
+ # Are there no elements present?
+ def empty?
+ size == 0
+ end
+
+ # How many elements are present in the set?
+ def size
+ to_set.size
+ end
+
+ # Convert to an array
+ def to_a
+ to_set.to_a
+ end
+end
@@ -0,0 +1,95 @@
+class Meangirls::TwoPhaseSet < Meangirls::Set
+ def self.biases
+ ['r']
+ end
+
+ attr_accessor :a, :r
+ def initialize(hash = nil)
+ if hash
+ raise ArgumentError, "hash must contain a" unless hash['a']
+ raise ArgumentError, "hash must contain r" unless hash['r']
+
+ @a = Set.new hash['a']
+ @r = Set.new hash['r']
+ else
+ # Empty set
+ @a = Set.new
+ @r = Set.new
+ end
+ end
+
+ # Inserts e into the set. Raises ReinsertNotAllowed if e was previously
+ # deleted.
+ def <<(e)
+ if @r.include? e
+ raise Meangirls::ReinsertNotAllowed
+ end
+
+ @a << e
+ self
+ end
+
+ # Strict equality: both adds and removes for both 2p-sets are equal.
+ def ==(other)
+ other.kind_of? self.class and
+ a == other.a and
+ r == other.r
+ end
+
+ def as_json
+ {
+ 'type' => type,
+ 'a' => a.to_a,
+ 'r' => r.to_a
+ }
+ end
+
+ def bias
+ 'r'
+ end
+
+ def clone
+ c = super
+ c.a = a.clone
+ c.r = r.clone
+ c
+ end
+
+ # Deletes e from self. Raises DeleteNotAllowed if e does not presently
+ # exist.
+ def delete(e)
+ unless @a.include? e
+ raise Meangirls::DeleteNotAllowed
+ end
+ @r << e
+ e
+ end
+
+ # Merge with another 2p-set.
+ def merge(other)
+ unless other.kind_of? self.class
+ raise ArgumentError, "other must be a #{self.class}"
+ end
+
+ self.class.new(
+ 'a' => (a | other.a),
+ 'r' => (r | other.r)
+ )
+ end
+
+ def include?(e)
+ @a.include? e and not @r.include? e
+ end
+
+ def size
+ to_set.size
+ end
+
+ def to_set
+ @a - @r
+ end
+
+ def type
+ '2p-set'
+ end
+end
View
@@ -0,0 +1,16 @@
+require 'yajl/json_gem'
+
+shared :crdt do
+ should 'as_json' do
+ j = @s.as_json
+ j.should.be.kind_of? Hash
+ j['type'].should == @s.type
+ end
+
+ should 'round trip' do
+ @examples.each do |t|
+ Meangirls.parse(t.as_json).should == t
+ Meangirls.parse(JSON.parse(t.as_json.to_json)).should == t
+ end
+ end
+end
View
@@ -0,0 +1,6 @@
+require 'bacon'
+require "#{File.expand_path(File.dirname(__FILE__))}/crdt"
+require "#{File.expand_path(File.dirname(__FILE__))}/set"
+require "#{File.expand_path(File.dirname(__FILE__))}/../lib/meangirls"
+
+Bacon.summary_on_exit
View
@@ -0,0 +1,21 @@
+#!/usr/bin/env ruby
+
+require 'open3'
+require 'find'
+
+tests = []
+
+files, args = ARGV.partition do |arg|
+ File.exists? arg
+end
+
+dirs = files.empty? ? [File.dirname(__FILE__)] : files
+dirs.each do |dir|
+ Find.find(dir) do |path|
+ next unless path =~ /\.rb$/
+ next if path =~ /\/init\.rb$/
+ tests << path
+ end
+end
+
+system *(["bacon", "-r", File.expand_path(File.dirname(__FILE__) + "/init.rb")] + args + tests.sort)
Oops, something went wrong.

0 comments on commit cd835af

Please sign in to comment.