├── .gitignore ├── Source ├── tests.rb ├── string_reader.rb ├── string_matcher.rb └── state_builder.rb └── spec ├── string_matcher_spec.rb ├── string_reader_spec.rb └── state_builder_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | nbproject -------------------------------------------------------------------------------- /Source/tests.rb: -------------------------------------------------------------------------------- 1 | require 'state_builder' 2 | require 'StringReader' 3 | 4 | @state_builder = StateBuilder.new 5 | @string_reader = StringReader.new("a") 6 | @graph = @state_builder.create_from(@string_reader) -------------------------------------------------------------------------------- /Source/string_reader.rb: -------------------------------------------------------------------------------- 1 | class StringReader 2 | def initialize(string) 3 | @string = string 4 | @current_index = -1 5 | end 6 | 7 | def peek 8 | if end_of_string 9 | return nil 10 | end 11 | 12 | @string[@current_index + 1, 1] 13 | end 14 | 15 | def read 16 | if end_of_string 17 | raise Exception.new("Tried to read past end of string! Je n'aime pas que!") 18 | end 19 | 20 | @current_index += 1 21 | @string[@current_index, 1] 22 | end 23 | 24 | def read_each 25 | while !end_of_string 26 | yield read 27 | end 28 | end 29 | 30 | def end_of_string 31 | @current_index == @string.length - 1 32 | end 33 | end -------------------------------------------------------------------------------- /Source/string_matcher.rb: -------------------------------------------------------------------------------- 1 | require "string_reader" 2 | require "state_builder" 3 | 4 | class StringMatcher 5 | def is_match(pattern, test_string) 6 | state_builder = StateBuilder.new 7 | graph = state_builder.create_from(pattern) 8 | 9 | reader = StringReader.new(test_string) 10 | next_states = [graph] 11 | while !reader.end_of_string 12 | test_states = next_states 13 | next_states = [] 14 | current_character = reader.read 15 | 16 | test_states.each do |state| 17 | state.transitions.each do |transition| 18 | if transition.can_do(current_character) 19 | next_states << transition.destination 20 | end 21 | end 22 | end 23 | end 24 | 25 | next_states.each do |state| 26 | state.transitions.each do |transition| 27 | if transition.class == AlwaysTransition and transition.destination.class == TerminalState 28 | return true 29 | end 30 | end 31 | end 32 | 33 | return false 34 | end 35 | end -------------------------------------------------------------------------------- /spec/string_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # To change this template, choose Tools | Templates 2 | # and open the template in the editor. 3 | require 'string_matcher' 4 | 5 | describe "When matching a.c*z with abcdefghzq" do 6 | before(:each) do 7 | @string_matcher = StringMatcher.new 8 | @result = @string_matcher.is_match("a.c*z", "abcdefghzq") 9 | end 10 | 11 | it "should NOT match" do 12 | @result.should == false 13 | end 14 | end 15 | 16 | describe "When matching a.c*z with abcdefghz" do 17 | before(:each) do 18 | @string_matcher = StringMatcher.new 19 | @result = @string_matcher.is_match("a.c*z", "abcdefghz") 20 | end 21 | 22 | it "should match" do 23 | @result.should == true 24 | end 25 | end 26 | 27 | describe "When matching z.c with abc" do 28 | before(:each) do 29 | @string_matcher = StringMatcher.new 30 | @result = @string_matcher.is_match("z.c", "abc") 31 | end 32 | 33 | it "should NOT match" do 34 | @result.should == false 35 | end 36 | end 37 | 38 | describe "When matching a.c with abc" do 39 | before(:each) do 40 | @string_matcher = StringMatcher.new 41 | @result = @string_matcher.is_match("a.c", "abc") 42 | end 43 | 44 | it "should match" do 45 | @result.should == true 46 | end 47 | end 48 | 49 | describe "When matching a with b" do 50 | before(:each) do 51 | @string_matcher = StringMatcher.new 52 | @result = @string_matcher.is_match("a", "b") 53 | end 54 | 55 | it "should NOT match" do 56 | @result.should == false 57 | end 58 | end 59 | 60 | describe "When matching a with a" do 61 | before(:each) do 62 | @string_matcher = StringMatcher.new 63 | @result = @string_matcher.is_match("a", "a") 64 | end 65 | 66 | it "should match" do 67 | @result.should == true 68 | end 69 | end 70 | 71 | -------------------------------------------------------------------------------- /spec/string_reader_spec.rb: -------------------------------------------------------------------------------- 1 | require 'Source/string_reader' 2 | 3 | alias running lambda 4 | 5 | describe "When reading each character in string" do 6 | before(:each) do 7 | @string_reader = StringReader.new("abc") 8 | @my_result = "" 9 | @string_reader.read_each do |character| 10 | @my_result << character 11 | end 12 | end 13 | 14 | it "should iterate through each character in the correct order" do 15 | @my_result.should == "abc" 16 | end 17 | end 18 | 19 | describe "When reading past last character in the string" do 20 | before(:each) do 21 | @string_reader = StringReader.new("ab") 22 | @string_reader.read 23 | @string_reader.read 24 | end 25 | 26 | it "should asplode!" do 27 | running{@string_reader.read}.should raise_error 28 | end 29 | end 30 | 31 | describe "When peeking after reading last character in the string" do 32 | before(:each) do 33 | @string_reader = StringReader.new("ab") 34 | @string_reader.read 35 | @string_reader.read 36 | @peeked_string = @string_reader.peek 37 | end 38 | 39 | it "should return None" do 40 | @peeked_string.should == nil 41 | end 42 | end 43 | 44 | describe "When reading last character in the string" do 45 | before(:each) do 46 | @string_reader = StringReader.new("ab") 47 | @string_reader.read 48 | @string_reader.read 49 | end 50 | 51 | it "should detect end of string" do 52 | @string_reader.end_of_string.should == true 53 | end 54 | end 55 | 56 | describe "When read from twice the StringReader" do 57 | before(:each) do 58 | @string_reader = StringReader.new("ab") 59 | @first_read_string = @string_reader.read 60 | @second_read_string = @string_reader.read 61 | end 62 | 63 | it "should return two different characters" do 64 | @first_read_string.should_not == @second_read_string 65 | end 66 | end 67 | 68 | describe "When peeking from a multiletter string the StringReader" do 69 | before(:each) do 70 | @string_reader = StringReader.new("ab") 71 | @peeked_string = @string_reader.peek 72 | end 73 | 74 | it "should return only a single character" do 75 | @peeked_string.length.should == 1 76 | end 77 | 78 | it "should match the first letter of the string" do 79 | @peeked_string.should == "a" 80 | end 81 | end 82 | 83 | describe "When StringReader is read from the string read" do 84 | before(:each) do 85 | @string_reader = StringReader.new("a") 86 | @peeked_string = @string_reader.peek 87 | @read_string = @string_reader.read 88 | end 89 | 90 | it "should be the peeked string" do 91 | @read_string.should == @peeked_string 92 | end 93 | end 94 | 95 | describe "When StringReader is initialized and peeked" do 96 | before(:each) do 97 | @string_reader = StringReader.new("a") 98 | @peeked_string = @string_reader.peek 99 | end 100 | 101 | it "should return the first character in the read string" do 102 | @peeked_string.should == "a" 103 | end 104 | end -------------------------------------------------------------------------------- /Source/state_builder.rb: -------------------------------------------------------------------------------- 1 | require "string_reader" 2 | 3 | class StateBuilder 4 | def initialize 5 | @number_created 6 | end 7 | 8 | def create_from(pattern_string) 9 | string_reader = StringReader.new(pattern_string) 10 | root = _create_start_state 11 | current_state = root 12 | 13 | string_reader.read_each do |current_character| 14 | current_transition = create_transition(current_character, current_state) 15 | current_state.add_transition(current_transition) 16 | current_state = current_transition.destination 17 | end 18 | 19 | final_state = _create_terminal_state 20 | final_transition = AlwaysTransition.new(final_state, nil) 21 | current_state.add_transition(final_transition) 22 | 23 | return root 24 | end 25 | 26 | def create_transition(match_token, previous_state) 27 | case match_token 28 | when "." then MatchAnyTransition.new(_create_standard_state, match_token) 29 | when "*" then MatchAnyTransition.new(previous_state, match_token) 30 | else LiteralTransition.new(_create_standard_state, match_token) 31 | end 32 | end 33 | 34 | def _create_terminal_state 35 | @number_created += 1 36 | result = TerminalState.new(@number_created) 37 | return result 38 | end 39 | 40 | def _create_start_state 41 | @number_created = 0 42 | result = StartState.new(@number_created) 43 | return result 44 | end 45 | 46 | def _create_standard_state 47 | @number_created += 1 48 | result = StandardState.new(@number_created) 49 | return result 50 | end 51 | end 52 | 53 | class StandardState 54 | def transitions 55 | @transitions 56 | end 57 | 58 | def id 59 | @id 60 | end 61 | 62 | def initialize(id) 63 | @id = id 64 | @transitions = [] 65 | end 66 | 67 | def add_transition(transition) 68 | if transition == nil 69 | raise Exception.new("transition can NOT be nil!!!") 70 | end 71 | 72 | if @transitions == nil 73 | raise Exception.new("@transition can NOT be nil!!!") 74 | end 75 | 76 | @transitions << transition 77 | end 78 | end 79 | 80 | class StandardState 81 | def transitions 82 | @transitions 83 | end 84 | 85 | def id 86 | @id 87 | end 88 | 89 | def initialize(id) 90 | @id = id 91 | @transitions = [] 92 | end 93 | 94 | def add_transition(transition) 95 | if transition == nil 96 | raise Exception.new("transition can NOT be nil!!!") 97 | end 98 | 99 | if @transitions == nil 100 | raise Exception.new("@transition can NOT be nil!!!") 101 | end 102 | 103 | @transitions << transition 104 | end 105 | end 106 | 107 | class TerminalState < StandardState 108 | def initialize(id) 109 | @id = id 110 | @transitions = [] 111 | end 112 | end 113 | 114 | class StartState