fae 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +20 -0
- data/examples/advanced.rb +21 -12
- data/examples/intersection_union_difference.rb +12 -32
- data/lib/fae.rb +1 -0
- data/lib/fae/exceptions.rb +24 -0
- data/lib/fae/finite_automata.rb +51 -54
- data/lib/fae/language.rb +1 -6
- data/lib/fae/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a353faa342e9405b48cfa833f5069cf5161dc7e4
|
4
|
+
data.tar.gz: 67eb919d556ffc2d0dbe6ad08e4f6ac1a703fbe3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 467117d43bb395ad9c0dd1fe975014edc1a57420dc683bbd8ddf98f0f70115b115d841d7cdaf63185dfc85c909840b5b05e5836a08dedf872a8e6e3c6202808a
|
7
|
+
data.tar.gz: 6dd10a31af3524a001c508f0177ee2dabdc9980fe246e0a1393960699d4712ceed8ead1bcf8027202c22fde4ca3d36bec1ac9477a64a231a1f1314709c0f114f
|
data/README.md
CHANGED
@@ -138,6 +138,20 @@ valid = fa.evaluate!(true)
|
|
138
138
|
puts "Diagram is correct" if valid
|
139
139
|
```
|
140
140
|
|
141
|
+
The add and evaluate methods can be chained also:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
states = [
|
145
|
+
# Some states...
|
146
|
+
]
|
147
|
+
|
148
|
+
strings = [
|
149
|
+
# Some strings...
|
150
|
+
]
|
151
|
+
|
152
|
+
fa.add_states(states).add_strings(strings).evaluate!
|
153
|
+
```
|
154
|
+
|
141
155
|
### Generating Tests for your State Diagram
|
142
156
|
|
143
157
|
If you really want to test your diagram and don't want to have to come up with all of your valid or invalid strings, you can generate valid and invalid strings by setting the `valid_block` as a lambda returning a boolean expression on your Finite Automata that represents a valid string in the language.
|
@@ -166,6 +180,12 @@ fa.generate_strings(1000, 20)
|
|
166
180
|
fa.evaluate!
|
167
181
|
```
|
168
182
|
|
183
|
+
These can be chained also:
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
fa.generate_strings(1000, 20).evaluate!
|
187
|
+
```
|
188
|
+
|
169
189
|
For more examples, see [`examples/advanced.rb`](https://github.com/caseyscarborough/fae/blob/master/examples/advanced.rb)
|
170
190
|
|
171
191
|
## When Your State Diagram is Incorrect
|
data/examples/advanced.rb
CHANGED
@@ -3,6 +3,17 @@ require_relative '../lib/fae'
|
|
3
3
|
CHARACTERS = ['a', 'b', 'c', 'd']
|
4
4
|
LANGUAGE = Fae::Language.new(CHARACTERS)
|
5
5
|
|
6
|
+
# Patch the string class to make valid lambdas easier to read.
|
7
|
+
class String
|
8
|
+
def has_at_least_three_cs?
|
9
|
+
self.length - 3 >= self.gsub('c', '').length
|
10
|
+
end
|
11
|
+
|
12
|
+
def has_abcd_substring?
|
13
|
+
!self.match(/abcd/).nil?
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
6
17
|
def print_valid_message(valid)
|
7
18
|
message = valid ? "VALID".colorize(:green) : "INVALID".colorize(:red)
|
8
19
|
puts message
|
@@ -10,9 +21,7 @@ end
|
|
10
21
|
|
11
22
|
def run_validation(fa, block)
|
12
23
|
fa.valid_block = block
|
13
|
-
fa.generate_strings(100, 20)
|
14
|
-
|
15
|
-
valid = fa.evaluate!(true)
|
24
|
+
valid = fa.generate_strings(100, 20).evaluate!(true)
|
16
25
|
print_valid_message(valid)
|
17
26
|
end
|
18
27
|
|
@@ -51,18 +60,18 @@ run_validation(fa_2, lambda { |string| string.length - 3 >= string.gsub('c', '')
|
|
51
60
|
|
52
61
|
# Intersection
|
53
62
|
intersection = fa_1.intersection(fa_2)
|
54
|
-
run_validation(
|
55
|
-
|
56
|
-
|
63
|
+
run_validation(
|
64
|
+
intersection, lambda { |s| s.has_abcd_substring? && s.has_at_least_three_cs? }
|
65
|
+
)
|
57
66
|
|
58
67
|
# Union
|
59
68
|
union = fa_1.union(fa_2)
|
60
|
-
run_validation(
|
61
|
-
|
62
|
-
|
69
|
+
run_validation(
|
70
|
+
union, lambda { |s| s.has_abcd_substring? || s.has_at_least_three_cs? }
|
71
|
+
)
|
63
72
|
|
64
73
|
# Difference
|
65
74
|
difference = fa_1.difference(fa_2)
|
66
|
-
run_validation(
|
67
|
-
|
68
|
-
|
75
|
+
run_validation(
|
76
|
+
difference, lambda { |s| s.has_abcd_substring? && !s.has_at_least_three_cs? }
|
77
|
+
)
|
@@ -20,32 +20,6 @@ def string_has_bb_substring?(string)
|
|
20
20
|
!string.match(/bb/).nil?
|
21
21
|
end
|
22
22
|
|
23
|
-
def valid_for_intersection?(string)
|
24
|
-
string_has_odd_number_of_the_letter_a?(string) && string_has_bb_substring?(string)
|
25
|
-
end
|
26
|
-
|
27
|
-
def valid_for_union?(string)
|
28
|
-
string_has_odd_number_of_the_letter_a?(string) || string_has_bb_substring?(string)
|
29
|
-
end
|
30
|
-
|
31
|
-
def valid_for_difference?(string)
|
32
|
-
string_has_odd_number_of_the_letter_a?(string) && !string_has_bb_substring?(string)
|
33
|
-
end
|
34
|
-
|
35
|
-
# Returns 100 random strings in the language, 50 of length 15 and 50 of length 5.
|
36
|
-
def get_random_strings
|
37
|
-
generate_string = lambda do |num|
|
38
|
-
string = ""
|
39
|
-
num.times { string << CHARACTERS.sample }
|
40
|
-
string
|
41
|
-
end
|
42
|
-
|
43
|
-
strings = []
|
44
|
-
50.times { strings << generate_string.call(15) }
|
45
|
-
50.times { strings << generate_string.call(5) }
|
46
|
-
strings
|
47
|
-
end
|
48
|
-
|
49
23
|
fa_1 = Fae::FiniteAutomata.new(LANGUAGE, "the language of all strings where the number of a's is odd")
|
50
24
|
fa_2 = Fae::FiniteAutomata.new(LANGUAGE, "the language of all strings that include the substring 'bb'")
|
51
25
|
|
@@ -64,15 +38,21 @@ fa_2.add_states([
|
|
64
38
|
|
65
39
|
# Perform the intersection
|
66
40
|
intersection = fa_1.intersection(fa_2)
|
67
|
-
|
68
|
-
|
41
|
+
intersection.valid_block = lambda do |string|
|
42
|
+
string_has_odd_number_of_the_letter_a?(string) && string_has_bb_substring?(string)
|
43
|
+
end
|
44
|
+
intersection.generate_strings(100, 10).evaluate!
|
69
45
|
|
70
46
|
# Perform the union
|
71
47
|
union = fa_1.union(fa_2)
|
72
|
-
|
73
|
-
|
48
|
+
union.valid_block = lambda do |string|
|
49
|
+
string_has_odd_number_of_the_letter_a?(string) || string_has_bb_substring?(string)
|
50
|
+
end
|
51
|
+
union.generate_strings(100, 10).evaluate!
|
74
52
|
|
75
53
|
# Perform the difference
|
76
54
|
difference = fa_1.difference(fa_2)
|
77
|
-
|
78
|
-
|
55
|
+
difference.valid_block = lambda do |string|
|
56
|
+
string_has_odd_number_of_the_letter_a?(string) && !string_has_bb_substring?(string)
|
57
|
+
end
|
58
|
+
difference.generate_strings(100, 10).evaluate!
|
data/lib/fae.rb
CHANGED
@@ -0,0 +1,24 @@
|
|
1
|
+
module Fae
|
2
|
+
# Generic exception for the Fae library.
|
3
|
+
class FaeException < StandardError; end
|
4
|
+
|
5
|
+
# Raised when Finite Automata doesn't have a valid block
|
6
|
+
# and strings are trying to be generated
|
7
|
+
class MissingValidBlockException < FaeException; end
|
8
|
+
|
9
|
+
# Raised when a state couldn't be found in a diagram,
|
10
|
+
# which should never happen.
|
11
|
+
class StateNotFoundException < FaeException; end
|
12
|
+
|
13
|
+
# Raised when a duplicate state is trying to be added
|
14
|
+
# to a FiniteAutomata.
|
15
|
+
class DuplicateStateException < FaeException; end
|
16
|
+
|
17
|
+
# Raised when trying to evaluate a FiniteAutomata
|
18
|
+
# with no states.
|
19
|
+
class EmptyStatesException < FaeException; end
|
20
|
+
|
21
|
+
# Raised when trying to perform a set operation on
|
22
|
+
# FiniteAutomata with two different languages.
|
23
|
+
class LanguageMismatchException < FaeException; end
|
24
|
+
end
|
data/lib/fae/finite_automata.rb
CHANGED
@@ -24,9 +24,8 @@ module Fae
|
|
24
24
|
#
|
25
25
|
# @param strings [Array] an array of strings
|
26
26
|
def add_strings(strings)
|
27
|
-
strings.each
|
28
|
-
|
29
|
-
end
|
27
|
+
strings.each { |string| @strings << string }
|
28
|
+
self
|
30
29
|
end
|
31
30
|
|
32
31
|
# Adds a single string to the strings array.
|
@@ -35,24 +34,24 @@ module Fae
|
|
35
34
|
def add_string(string)
|
36
35
|
@strings << string
|
37
36
|
end
|
38
|
-
|
39
|
-
def add_state(new_state)
|
40
|
-
valid = true
|
41
|
-
@states.each do |state|
|
42
|
-
if (new_state.name == state.name)
|
43
|
-
raise 'Duplicate state added for Finite Automata'
|
44
|
-
end
|
45
|
-
end
|
46
|
-
@states << new_state
|
47
|
-
end
|
48
37
|
|
49
38
|
# Adds strings to check against when evaluating.
|
50
39
|
#
|
51
40
|
# @param states [Array] an array of states
|
52
41
|
def add_states(states)
|
53
|
-
states.each
|
54
|
-
|
42
|
+
states.each { |state| add_state(state) }
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
# Adds a single state to the states array.
|
47
|
+
#
|
48
|
+
# @param new_state [State] the state to add.
|
49
|
+
def add_state(new_state)
|
50
|
+
valid = true
|
51
|
+
@states.each do |state|
|
52
|
+
raise Fae::DuplicateStateException, 'Duplicate state added for Finite Automata' if new_state.name == state.name
|
55
53
|
end
|
54
|
+
@states << new_state
|
56
55
|
end
|
57
56
|
|
58
57
|
# Retrieves a state from this finite automata by name.
|
@@ -67,9 +66,9 @@ module Fae
|
|
67
66
|
end
|
68
67
|
end
|
69
68
|
if (retrieved_state.nil?)
|
70
|
-
raise "State #{name} was not found in this Finite Automata. Ensure that all states have outputs for #{@language.characters}"
|
69
|
+
raise Fae::StateNotFoundException, "State #{name} was not found in this Finite Automata. Ensure that all states have outputs for #{@language.characters}"
|
71
70
|
end
|
72
|
-
|
71
|
+
retrieved_state
|
73
72
|
end
|
74
73
|
|
75
74
|
# Generates the finite automata for the intersection
|
@@ -98,32 +97,28 @@ module Fae
|
|
98
97
|
|
99
98
|
# Runs the evaluation on the finite automata.
|
100
99
|
def evaluate!(suppress_output=false)
|
101
|
-
|
100
|
+
raise Fae::EmptyStatesException, 'You must add some states to your Finite Automata before checking strings' if @states.length == 0
|
101
|
+
|
102
|
+
output = "Evaluating strings for #{@description} using language #{@language.characters}".colorize(:yellow)
|
102
103
|
@invalids = []
|
103
|
-
if (@states.length == 0)
|
104
|
-
raise "You must add some states to your Finite Automata before checking strings"
|
105
|
-
end
|
106
104
|
|
107
|
-
output << "Evaluating strings for #{@description} using language #{@language.characters}".colorize(:yellow)
|
108
105
|
@strings.each do |string|
|
109
106
|
result = evaluate_string(string)
|
110
|
-
if
|
111
|
-
|
112
|
-
end
|
113
|
-
output << result[:output]
|
107
|
+
@invalids << string if !result[:valid]
|
108
|
+
output << result[:output]
|
114
109
|
end
|
115
110
|
|
116
111
|
num_invalid = @invalids.length
|
117
112
|
valid = num_invalid == 0
|
118
|
-
if
|
119
|
-
output << "\nState diagram may be incorrect for #{@description}".colorize(:red)
|
120
|
-
output << "\
|
113
|
+
if !valid
|
114
|
+
output << "\nState diagram may be incorrect for #{@description}\n".colorize(:red)
|
115
|
+
output << "\nA total of #{num_invalid} string#{'s' if num_invalid != 1} did not meet your expectations:\n\n"
|
121
116
|
|
122
117
|
@invalids.each do |string|
|
123
118
|
expected_string = string.expected ? "valid".colorize(:green) : "invalid".colorize(:red)
|
124
|
-
output << "
|
119
|
+
output << "* You expected the string '#{string.colorize(:blue)}' to be #{expected_string}\n"
|
125
120
|
end
|
126
|
-
output << "\
|
121
|
+
output << "\nIf these expectations are correct, then your state diagram needs revising. Otherwise, you've simply expected the wrong values."
|
127
122
|
else
|
128
123
|
output << "\nState diagram is correct.\n".colorize(:green)
|
129
124
|
end
|
@@ -131,27 +126,29 @@ module Fae
|
|
131
126
|
if (!suppress_output)
|
132
127
|
puts output
|
133
128
|
end
|
134
|
-
|
129
|
+
valid
|
135
130
|
end
|
136
131
|
|
132
|
+
# Generates strings for the finite automata and adds them
|
133
|
+
# to the strings array.
|
134
|
+
#
|
135
|
+
# @param number [Integer] number of strings
|
136
|
+
# @param length [Integer] length of each string
|
137
137
|
def generate_strings(number, length)
|
138
|
-
if valid_block.nil?
|
139
|
-
|
140
|
-
end
|
141
|
-
|
138
|
+
raise Fae::MissingValidBlockException, 'You must set the valid block for the Finite Automata to generate strings' if valid_block.nil?
|
139
|
+
|
142
140
|
strings = []
|
143
141
|
number.times do
|
144
142
|
string = ""
|
145
143
|
length.times { string << @language.characters.sample }
|
146
144
|
strings << String.new(string, valid_block.call(string))
|
147
145
|
end
|
148
|
-
add_strings(strings)
|
149
|
-
|
146
|
+
self.add_strings(strings)
|
147
|
+
self
|
150
148
|
end
|
151
149
|
|
152
150
|
def to_s
|
153
|
-
output = "
|
154
|
-
output << "Description: ".colorize(:yellow) + @description
|
151
|
+
output = "Description: ".colorize(:yellow) + @description
|
155
152
|
output << "\nLanguage: ".colorize(:yellow) + language.characters.to_s
|
156
153
|
output << "\nStates:".colorize(:yellow)
|
157
154
|
@states.each do |state|
|
@@ -162,19 +159,19 @@ module Fae
|
|
162
159
|
accepting = state.accepting ? "accepting".colorize(:green) : "not accepting".colorize(:red)
|
163
160
|
output << "\n #{'~'.colorize(:yellow)} #{accepting}"
|
164
161
|
end
|
165
|
-
|
162
|
+
output
|
166
163
|
end
|
167
164
|
|
168
165
|
private
|
169
166
|
def perform_set_operation(type, fa)
|
170
|
-
if
|
171
|
-
|
172
|
-
return
|
167
|
+
if @language.characters.uniq.sort != fa.language.characters.uniq.sort
|
168
|
+
raise LanguageMismatchException, "Performing the #{type.to_s} requires the languages to be the same.".colorize(:red)
|
173
169
|
end
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
170
|
+
|
171
|
+
states = []
|
172
|
+
strings = []
|
173
|
+
description = "the #{type.to_s} of #{@description} and #{fa.description}"
|
174
|
+
new_fa = FiniteAutomata.new(@language, description)
|
178
175
|
|
179
176
|
@states.each do |state|
|
180
177
|
fa.states.each do |fa_state|
|
@@ -190,30 +187,30 @@ module Fae
|
|
190
187
|
(type == :union && (state.accepting || fa_state.accepting)))
|
191
188
|
accepting = true
|
192
189
|
end
|
193
|
-
|
190
|
+
states << State.new(name, paths, accepting)
|
194
191
|
end
|
195
192
|
end
|
196
193
|
|
197
|
-
|
198
|
-
|
194
|
+
new_fa.add_states(states)
|
195
|
+
new_fa
|
199
196
|
end
|
200
197
|
|
201
198
|
def evaluate_string(string)
|
199
|
+
valid = true
|
202
200
|
output = "\n#{string.colorize(:blue)}: "
|
203
201
|
if (@language.string_is_valid(string))
|
204
202
|
result = @states.first.evaluate(string, self)
|
205
203
|
output << result[:output]
|
206
204
|
if (result[:accepting] != string.expected)
|
207
205
|
output << "\u2717".encode("utf-8").colorize(:red)
|
208
|
-
|
206
|
+
valid = false
|
209
207
|
else
|
210
208
|
output << "\u2713".encode("utf-8").colorize(:green)
|
211
|
-
return { :valid => true, :output => output }
|
212
209
|
end
|
213
210
|
else
|
214
211
|
output << "not valid for language #{@characters}".colorize(:red)
|
215
|
-
return { :valid => true, :output => output }
|
216
212
|
end
|
213
|
+
return { :valid => valid, :output => output }
|
217
214
|
end
|
218
215
|
end
|
219
216
|
end
|
data/lib/fae/language.rb
CHANGED
@@ -17,12 +17,7 @@ module Fae
|
|
17
17
|
# @param string [String] the string to check
|
18
18
|
def string_is_valid(string)
|
19
19
|
# Use lookahead to check for valid string
|
20
|
-
|
21
|
-
|
22
|
-
if string.match /#{regex}/
|
23
|
-
return true
|
24
|
-
end
|
25
|
-
return false
|
20
|
+
string.match "^(?=.*\\D)[#{@characters.join('|')}]+$"
|
26
21
|
end
|
27
22
|
|
28
23
|
# Adds a character to the language.
|
data/lib/fae/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fae
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Casey Scarborough
|
@@ -77,6 +77,7 @@ files:
|
|
77
77
|
- examples/simple.rb
|
78
78
|
- fae.gemspec
|
79
79
|
- lib/fae.rb
|
80
|
+
- lib/fae/exceptions.rb
|
80
81
|
- lib/fae/finite_automata.rb
|
81
82
|
- lib/fae/language.rb
|
82
83
|
- lib/fae/state.rb
|