weft-qda 0.9.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/lib/weft.rb +21 -0
  2. data/lib/weft/WEFT-VERSION-STRING.rb +1 -0
  3. data/lib/weft/application.rb +130 -0
  4. data/lib/weft/backend.rb +39 -0
  5. data/lib/weft/backend/marshal.rb +26 -0
  6. data/lib/weft/backend/mysql.rb +267 -0
  7. data/lib/weft/backend/n6.rb +366 -0
  8. data/lib/weft/backend/sqlite.rb +633 -0
  9. data/lib/weft/backend/sqlite/category_tree.rb +104 -0
  10. data/lib/weft/backend/sqlite/schema.rb +152 -0
  11. data/lib/weft/backend/sqlite/upgradeable.rb +55 -0
  12. data/lib/weft/category.rb +157 -0
  13. data/lib/weft/coding.rb +355 -0
  14. data/lib/weft/document.rb +118 -0
  15. data/lib/weft/filters.rb +243 -0
  16. data/lib/weft/wxgui.rb +687 -0
  17. data/lib/weft/wxgui/category.xpm +26 -0
  18. data/lib/weft/wxgui/dialogs.rb +128 -0
  19. data/lib/weft/wxgui/document.xpm +25 -0
  20. data/lib/weft/wxgui/error_handler.rb +52 -0
  21. data/lib/weft/wxgui/inspectors.rb +361 -0
  22. data/lib/weft/wxgui/inspectors/category.rb +165 -0
  23. data/lib/weft/wxgui/inspectors/codereview.rb +275 -0
  24. data/lib/weft/wxgui/inspectors/document.rb +139 -0
  25. data/lib/weft/wxgui/inspectors/imagedocument.rb +56 -0
  26. data/lib/weft/wxgui/inspectors/script.rb +35 -0
  27. data/lib/weft/wxgui/inspectors/search.rb +265 -0
  28. data/lib/weft/wxgui/inspectors/textcontrols.rb +304 -0
  29. data/lib/weft/wxgui/lang.rb +17 -0
  30. data/lib/weft/wxgui/lang/en.rb +45 -0
  31. data/lib/weft/wxgui/mondrian.xpm +44 -0
  32. data/lib/weft/wxgui/search.xpm +25 -0
  33. data/lib/weft/wxgui/sidebar.rb +498 -0
  34. data/lib/weft/wxgui/utilities.rb +148 -0
  35. data/lib/weft/wxgui/weft16.xpm +31 -0
  36. data/lib/weft/wxgui/workarea.rb +249 -0
  37. data/test/001-document.rb +196 -0
  38. data/test/002-category.rb +138 -0
  39. data/test/003-code.rb +370 -0
  40. data/test/004-application.rb +52 -0
  41. data/test/006-filters.rb +139 -0
  42. data/test/009a-backend_sqlite_basic.rb +280 -0
  43. data/test/009b-backend_sqlite_complex.rb +175 -0
  44. data/test/009c_backend_sqlite_bench.rb +81 -0
  45. data/test/010-backend_nudist.rb +5 -0
  46. data/test/all-tests.rb +1 -0
  47. data/test/manual-gui-script.txt +24 -0
  48. data/test/testdata/autocoding-test.txt +15 -0
  49. data/test/testdata/iso-8859-1.txt +5 -0
  50. data/test/testdata/sample_doc.txt +19 -0
  51. data/test/testdata/search_results.txt +1254 -0
  52. data/test/testdata/text1-dos-ascii.txt +2 -0
  53. data/test/testdata/text1-unix-utf8.txt +2 -0
  54. data/weft-qda.rb +28 -0
  55. metadata +96 -0
@@ -0,0 +1,148 @@
1
+ module QDA::GUI
2
+ # ItemData - allows widgets to store information about assocations
3
+ # with underlying project objects such as categories and documents.
4
+ module ItemData
5
+ # for faked-up item data
6
+ def get_item_data(id)
7
+ @data_table[id]
8
+ end
9
+ alias :get_client_data :get_item_data
10
+
11
+ # for faked-up item data
12
+ def set_item_data(id, data)
13
+ @data_table ||= Hash.new()
14
+ @data_table[id] = data
15
+ return nil
16
+ end
17
+ alias :set_client_data :set_item_data
18
+
19
+ # returns the key associated with the value +value+
20
+ def value_to_ident(value)
21
+ return nil if value.nil?
22
+ unless value.respond_to?(:dbid)
23
+ Kernel.raise "Cannot search for invalid value #{value.inspect}"
24
+ end
25
+ doc = @data_table.each do | k, val |
26
+ return k if val.dbid == value.dbid
27
+ end
28
+ return nil
29
+ end
30
+ end
31
+
32
+
33
+ # Allows an object such as the global Wx App to broadcast messages
34
+ # about changes in the database and other application states that
35
+ # might require the child to update its appearance.
36
+ module Broadcaster
37
+ SUBSCRIBABLE_EVENTS = [ :document_added, :document_changed,
38
+ :document_deleted, :category_added, :category_changed,
39
+ :category_deleted, :focus_category, :savestate_changed ]
40
+
41
+ # intended to be module private variables to managed which widgets
42
+ # are subscribing to which events
43
+ attr_accessor :bc__subscribers, :bc__subscriptions
44
+
45
+ # broadcast an event to all subscribers to event of type
46
+ # +event_type+, optionally including +content+
47
+ def broadcast(event_type, content = nil)
48
+ for subscriber in bc__subscriptions[event_type]
49
+ subscriber.notify(event_type, content)
50
+ end
51
+ end
52
+
53
+ # subscribe the widget +subscriber+ to receive notification of
54
+ # app event types +*events+ (which should be a set of symbols)
55
+ def add_subscriber(subscriber, *events)
56
+ events.each do | e |
57
+ bc__subscriptions[e].push(subscriber)
58
+ end
59
+ subscriber.evt_close do | e |
60
+ delete_subscriber(subscriber)
61
+ e.skip()
62
+ end
63
+ end
64
+
65
+ # accepts a ruby item
66
+ def delete_subscriber(subscriber)
67
+ if subscriber.respond_to?(:associated_subscribers)
68
+ subscriber.associated_subscribers.each do | child |
69
+ delete_subscriber(child)
70
+ end
71
+ end
72
+ bc__subscribers.delete(subscriber)
73
+ bc__subscriptions.each_value do | subscriber_set |
74
+ subscriber_set.delete(subscriber)
75
+ end
76
+ end
77
+
78
+ def Broadcaster.extended(obj)
79
+ $wxapp = obj
80
+ # this is where module-private variables would be great ..
81
+ obj.reset_subscriptions()
82
+ end
83
+
84
+ def reset_subscriptions()
85
+ self.bc__subscribers = []
86
+ self.bc__subscriptions = Hash.new do | ev |
87
+ raise "Cannot subscribe to unknown event #{ev}"
88
+ end
89
+ SUBSCRIBABLE_EVENTS.each { | ev | self.bc__subscriptions[ev] = [] }
90
+ end
91
+ end
92
+
93
+ # Any gui element that may need to modify its appearance in response
94
+ # to application updates may include the subscriber module. Instances
95
+ # of the class may then call the +subscribe+ method to receive
96
+ # notification of changes.
97
+ module Subscriber
98
+ # receive notification that an event of type +ev+ has been called,
99
+ # optionally passing +content+ for additional information about the
100
+ # object the event concerned.
101
+ def notify(ev, content = nil)
102
+ receiver = "receive_#{ev}".intern
103
+ if respond_to?(receiver)
104
+ send(receiver, content)
105
+ else
106
+ warn "#{self} received unhandled event #{ev}"
107
+ end
108
+ end
109
+
110
+ # Subscribe this object to the events +events+, which should be a
111
+ # list of symbols. For example, to receive notification of category
112
+ # changes and additions, the recipient shoudl call
113
+ # subscribe(:category_changed, :category_added)
114
+ #
115
+ # For every event type to which a subscriber subscribes, it should
116
+ # implement a corresponding receive_xxx method, where xxx is the
117
+ # name of the event type. To act upon category changes, the
118
+ # subscriber should implement +receive_category_changed+
119
+ def subscribe(*events)
120
+ $wxapp.add_subscriber(self, *events)
121
+ end
122
+ end
123
+
124
+ # extension to Wx::Colour
125
+ class Wx::Colour
126
+ # Create a new colour by mixing +self_parts+ of +self+ with
127
+ # +other_parts+ of other. The new colour is produced by applying the
128
+ # following averaging formula to the red, green and blue components
129
+ # of each colour
130
+ #
131
+ # ( colour_1 * conc_1 ) + ( colour_2 * conc_2 )
132
+ # --------------------------
133
+ # conc_1 + conc_2
134
+ #
135
+
136
+ def mix(other, self_parts = 1, other_parts = 1)
137
+ return self if self_parts.zero? && other_parts.zero?
138
+ rgb = [ :red, :green, :blue ].map do | component |
139
+ mix = self.send(component) * self_parts
140
+ mix += other.send(component) * other_parts
141
+ mix = mix.to_f
142
+ mix /= self_parts + other_parts
143
+ mix.to_i
144
+ end
145
+ Wx::Colour.new(*rgb)
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,31 @@
1
+ /* XPM */
2
+ static char *weft16[] = {
3
+ /* columns rows colors chars-per-pixel */
4
+ "16 16 9 1",
5
+ " c #400000",
6
+ ". c #CCCC00",
7
+ "X c #E9E991",
8
+ "o c #999900",
9
+ "O c gray100",
10
+ "+ c black",
11
+ "@ c black",
12
+ "# c black",
13
+ "$ c None",
14
+ /* pixels */
15
+ "$$$$$ $$$ $$$$",
16
+ "$$$$ XX $ XX $$$",
17
+ "$$$$ .XX XX. $$$",
18
+ "$$ $ .XX o $ $$",
19
+ "$ XX $ .XX $ X $",
20
+ "$ XX. o .X. XX. ",
21
+ "$$ . XXo . XX. $",
22
+ "$$$ XX. $ XX. $$",
23
+ "$$ XX. o XX. o $",
24
+ "$ XX. o.X . o.X ",
25
+ " XX. $ XXX $ X $",
26
+ " X. $ o XXX $ $$",
27
+ "$ $ XXo XXX $$$",
28
+ "$$$ XX. $ XX $$$",
29
+ "$$$ X. $$$ $$$$",
30
+ "$$$$ $$$$$$$$$$"
31
+ };
@@ -0,0 +1,249 @@
1
+ module QDA::GUI
2
+ # The main application window, where docuemnts, categories, searches
3
+ # etc are displayed. Only one instance per application.
4
+ class WorkArea < Wx::MDIParentFrame
5
+ include Subscriber
6
+
7
+ attr_accessor :app
8
+ def initialize(wx_app)
9
+ @wx_app = wx_app
10
+ @window_map = {}
11
+ super(nil, -1, "Weft QDA")
12
+ restore_size()
13
+ subscribe(:savestate_changed)
14
+ end
15
+
16
+ # Resizes the workarea to the state when the application was last used
17
+ def restore_size()
18
+ conf = Wx::ConfigBase::get()
19
+ conf.path = "/MainFrame"
20
+ x = conf.read_int("x", 200)
21
+ y = conf.read_int("y", 0)
22
+ w = conf.read_int("w", 300)
23
+ h = conf.read_int("h", 400)
24
+ max = conf.read_bool("max", false)
25
+
26
+ if max
27
+ maximize(true)
28
+ else
29
+ move( Wx::Point.new(x, y) )
30
+ set_size( Wx::Size.new(w, h) )
31
+ end
32
+ end
33
+
34
+ # save layout to Config so window is same position next time
35
+ def remember_size()
36
+ conf = Wx::ConfigBase::get()
37
+ # global window size settings
38
+ if conf
39
+ size = get_size()
40
+ pos = get_position
41
+ conf.path = '/MainFrame'
42
+ conf.write("x", pos.x)
43
+ conf.write("y", pos.y)
44
+ conf.write("w", size.width)
45
+ conf.write("h", size.height)
46
+ conf.write("max", is_maximized)
47
+ end
48
+ end
49
+
50
+ # returns a Wx::Size object that has a width equal to that of the
51
+ # current window times +w+, and a height equal to that of the
52
+ # current window times +h+. This is used to help size MDI children
53
+ # when they are first created.
54
+ def proportional_size(w, h)
55
+ size = client_size()
56
+ Wx::Size.new( ( size.get_width * w ).to_i,
57
+ ( size.get_height * h ).to_i )
58
+ end
59
+
60
+ # launch a child window of the type +klass+ (which should be a
61
+ # constant of the name of a subclass of QDA::GUI::InspectorWindow),
62
+ # optionally bound to the database object +bound_obj+ (typically a
63
+ # document or category).
64
+ # Returns a reference to the newly opened inspector window.
65
+ def launch_window(klass, bound_obj)
66
+ win = nil
67
+ Wx::BusyCursor.busy(self) do
68
+ if win = get_open_window(bound_obj)
69
+ win.activate()
70
+ else
71
+ w_i = window_ident(bound_obj)
72
+ last_layout = restore_layout( w_i )
73
+ win = klass.new( bound_obj, @wx_app, self, last_layout )
74
+ @window_map[w_i] = win
75
+
76
+ # set up a hook to clean up when this window is closed later
77
+ win.evt_close() do | e |
78
+ remember_layout(w_i, win)
79
+ @window_map.delete(w_i)
80
+ win.hide()
81
+ e.skip()
82
+ end
83
+ win.activate()
84
+ end
85
+ end
86
+ # should go over D & C window now
87
+ self.raise()
88
+ return win
89
+ end
90
+
91
+ def set_active_category(category)
92
+ @window_map.each do | name, win |
93
+ if win.respond_to?(:set_active_category)
94
+ win.set_active_category(category)
95
+ end
96
+ end
97
+ end
98
+
99
+ # turns a QDA object into a string window identifier a bit messy b/c
100
+ # has to deal with Queries (which aren't currently saved in
101
+ # database) and categories and documents (which are).
102
+ def window_ident(bound_obj)
103
+ bound_obj_id = bound_obj.dbid ? bound_obj.dbid : ''
104
+ # deal with either full or partial class names
105
+ bound_obj.class.name.sub(/^.*\:/, '') << bound_obj_id.to_s
106
+ end
107
+ private :window_ident
108
+
109
+ # gets the open MDI window representing +bound_obj+, if there is one
110
+ def get_open_window(bound_obj)
111
+ @window_map[ window_ident(bound_obj) ]
112
+ end
113
+
114
+
115
+ def get_open_windows(type_of)
116
+ if type_of == QDA::Document
117
+ doc_wins = @window_map.keys.grep(/^doc-/)
118
+ return @window_map.values_at( *doc_wins )
119
+ elsif type_of == QDA::Category
120
+ cat_wins = @window_map.keys.grep(/^cat-/)
121
+ return @window_map.values_at( *cat_wins )
122
+ else
123
+ return []
124
+ end
125
+ end
126
+
127
+ # remember how the windows were arranged
128
+ def remember_layouts()
129
+ # project specific settings
130
+ if @app
131
+ @window_map.each do | key, window |
132
+ remember_layout(key, window, true) unless key.nil?
133
+ end
134
+ end
135
+ end
136
+
137
+ def remember_layout(window_id, window, open = false)
138
+ if @app
139
+ layouts = @app.get_preference('Layouts') || {}
140
+ layouts[window_id] = window.layout
141
+ layouts[window_id][:open] = open
142
+ @app.save_preference('Layouts', layouts)
143
+ end
144
+ end
145
+
146
+ def restore_layout(window_id)
147
+ if @app
148
+ layouts = @app.get_preference('Layouts') || {}
149
+ return layouts[window_id]
150
+ end
151
+ end
152
+
153
+ def restore_layouts()
154
+ if @app
155
+ layouts = @app.get_preference('Layouts')
156
+ return unless layouts
157
+ # get all previously open windows
158
+ open = layouts.find_all { | x | x[1][:open] }
159
+ open.each do | ident, layout |
160
+ if ident =~ /^Document(\w+)$/
161
+ $wxapp.on_document_open($1)
162
+ elsif ident =~ /^Category(\w+)/ and
163
+ cat = @app.get_category($1, true)
164
+ $wxapp.on_category_open(cat)
165
+ end
166
+ end
167
+ end
168
+ end
169
+
170
+ def set_display_font(font)
171
+ # super(font)
172
+ @window_map.values.each do | child |
173
+ child.set_display_font(font) if child.respond_to?(:set_display_font)
174
+ end
175
+ end
176
+
177
+ def close_all()
178
+ @window_map.values.each { | child | child.close() }
179
+ end
180
+
181
+ def receive_savestate_changed(app)
182
+ title = 'Weft QDA'
183
+ if app
184
+ if app.dbfile
185
+ title << " - #{app.dbfile}"
186
+ else
187
+ title << " - [New Project]"
188
+ end
189
+ if app.dirty?
190
+ title << "*"
191
+ end
192
+ end
193
+ set_title(title)
194
+ end
195
+ end
196
+
197
+ class EasyMenu < Wx::Menu
198
+ def EasyMenu.next_base()
199
+ @base_id ||= 0
200
+ @base_id += 100
201
+ end
202
+
203
+ def initialize(target)
204
+ super()
205
+ @target = target
206
+ @base_id = EasyMenu.next_base()
207
+ @commands = Hash.new do | hash, key |
208
+ Kernel.raise ArgumentError.new("No such menu item #{key}")
209
+ end
210
+ end
211
+
212
+ # adds the item +command_str+ to the menu, with the optional
213
+ # shortcut key +command_key+, and sets it to run the associated
214
+ # block when the menu item is chosen. The menu item can later be
215
+ # referred to by the symbol with the commands name with special
216
+ # characters removed andspaces turned to underscores. For
217
+ # example, "Save Project" becomes :save_project
218
+ def add_item(command_str, command_key = '', itemtype = Wx::ITEM_NORMAL)
219
+ const = ( @base_id += 1 )
220
+ ident = command_str.gsub(/\s+/, "_").gsub(/\W/, "").downcase.to_sym
221
+ @commands[ident] = const
222
+ append(const, "#{command_str}\t#{command_key}", "", itemtype)
223
+ @target.evt_menu(const) { | e | yield(e) }
224
+ return ident
225
+ end
226
+
227
+ # pass a series of symbol idents eg :save_project, :close
228
+ def enable_items(*idents)
229
+ idents.each { | ident | enable( @commands[ident], true ) }
230
+ end
231
+ alias :enable_item :enable_items
232
+
233
+ def disable_items(*idents)
234
+ idents.each { | ident | enable( @commands[ident], false ) }
235
+ end
236
+ alias :disable_item :disable_items
237
+
238
+ def check_items(*idents)
239
+ idents.each { | ident | check( @commands[ident], true ) }
240
+ end
241
+ alias :check_item :check_items
242
+
243
+ def uncheck_items(*idents)
244
+ idents.each { | ident | check( @commands[ident], false ) }
245
+ end
246
+ alias :uncheck_item :uncheck_items
247
+
248
+ end
249
+ end
@@ -0,0 +1,196 @@
1
+ $:.push('../lib/')
2
+
3
+ require 'weft/document'
4
+ require 'test/unit'
5
+ require 'weft/category'
6
+
7
+ class TestDocument < Test::Unit::TestCase
8
+ def setup
9
+
10
+ end
11
+
12
+ def test_basic
13
+ d = QDA::Document.new('The Title')
14
+ assert_equal('The Title', d.title,
15
+ 'Set title at initialisation')
16
+ assert_equal(0, d.length,
17
+ 'Document length is 0')
18
+
19
+ d.append('ego')
20
+ assert_equal(4, d.length,
21
+ 'Document length is 4')
22
+ assert_equal('eg', d[0,2],
23
+ 'Retrieved doc substring')
24
+ assert_equal('go', d[1,2],
25
+ 'Retrieved doc substring')
26
+ assert_equal('', d[4,5],
27
+ 'Retrieved doc substring')
28
+ assert_equal('', d[2,0],
29
+ 'Retrieved doc substring')
30
+
31
+ d.append('bar')
32
+ assert_equal(8, d.length,
33
+ 'Document length is 8')
34
+ assert_equal("o\nba", d[2,4],
35
+ 'Retrieved doc substring')
36
+ assert_equal("ba", d[4,2],
37
+ 'Retrieved doc substring')
38
+ assert_equal("bar\n", d[4,9],
39
+ 'Retrieved doc substring')
40
+ end
41
+
42
+ def test_fragment
43
+ d = QDA::Document.new('The Title')
44
+ d.append('abc def ghi jkl')
45
+ d.append('ABC DEF GHI JKL')
46
+ fr_1 = d[0,4]
47
+ assert_equal(0, fr_1.offset, 'Give offset correctly')
48
+ assert_equal(4, fr_1.end, 'Give end correctly')
49
+ assert_equal('abc ', fr_1, 'String equality')
50
+
51
+ fr_2 = d[2,5]
52
+ assert_equal(2, fr_2.offset, 'Give offset correctly')
53
+ assert_equal(7, fr_2.end, 'Give end correctly')
54
+
55
+ fr_3 = d[12,6]
56
+ assert_equal(12, fr_3.offset, 'Give offset correctly')
57
+ assert_equal(18, fr_3.end, 'Give end correctly')
58
+
59
+ fr_3.docid = 1
60
+ code = fr_3.to_code()
61
+ assert_equal(1, code.docid)
62
+ assert_equal(12, code.offset)
63
+ assert_equal(6, code.length)
64
+ end
65
+
66
+ def test_fragment_equivalence
67
+ c_1 = QDA::Fragment.new('ABC', 'title', 5)
68
+ c_2 = QDA::Fragment.new('XYZ', 'title', 0)
69
+ c_3 = QDA::Fragment.new('ABC', 'other', 5)
70
+ c_4 = QDA::Fragment.new('ABC', 'title', 50)
71
+ c_5 = QDA::Fragment.new('ABC', 'title', 5)
72
+
73
+ assert(c_1 == c_1, 'Identity Equality')
74
+ assert(c_1 == c_5, 'Equivalence Equality')
75
+ assert(c_1 != c_2, 'Text inequality')
76
+ assert(c_1 != c_3, 'Document inequality')
77
+ assert(c_1 != c_4, 'Offset inequality')
78
+
79
+ c_1 = QDA::Fragment.new('ABCDEF', 'title', 5)
80
+ c_2 = QDA::Fragment.new('DEFGH', 'title', 8)
81
+
82
+ assert(c_1.overlap?(c_2), 'Overlap 1/2')
83
+ assert(c_2.overlap?(c_1), 'Overlap 1/1')
84
+ assert(c_1.touch?(c_2), 'Touch 1/1')
85
+ assert(c_2.touch?(c_1), 'Touch 1/2')
86
+
87
+ c_1 = QDA::Fragment.new('ABCDEF', 'title', 5)
88
+ c_2 = QDA::Fragment.new('GHIJKL', 'title', 11)
89
+
90
+ assert(! c_1.overlap?(c_2), 'Overlap 2/2')
91
+ assert(! c_2.overlap?(c_1), 'Overlap 2/1')
92
+ assert(c_1.touch?(c_2), 'Touch 2/1')
93
+ assert(c_2.touch?(c_1), 'Touch 2/2')
94
+
95
+ c_1 = QDA::Fragment.new('ABCDEF', 'title', 5)
96
+ c_2 = QDA::Fragment.new('JKL', 'title', 14)
97
+ assert(! c_1.overlap?(c_2), 'Overlap 3/2')
98
+ assert(! c_2.overlap?(c_1), 'Overlap 3/1')
99
+ assert(! c_1.touch?(c_2), 'Touch 3/1')
100
+ assert(! c_2.touch?(c_1), 'Touch 3/2')
101
+ end
102
+
103
+ def test_fragment_comparison
104
+ c_1 = QDA::Fragment.new('ABC', 'title', 5)
105
+ c_2 = QDA::Fragment.new('XYZ', 'title', 0)
106
+ assert(c_2 < c_1, 'Offset comparison')
107
+
108
+ cs = QDA::CodeSet[ c_1, c_2 ]
109
+ cs = cs.sort { | x, y | x <=> y }
110
+ assert_equal('XYZ', cs[0],
111
+ 'Sort test')
112
+
113
+ c_1 = QDA::Fragment.new('abc', 'title', 0)
114
+ c_2 = QDA::Fragment.new('xyz', 'title', 5)
115
+ assert(c_2 > c_1, 'Offset comparison')
116
+
117
+ cs = QDA::CodeSet[ c_1, c_2 ]
118
+ cs = cs.sort
119
+ assert_equal('abc', cs[0],
120
+ 'Sort test')
121
+ end
122
+
123
+ def test_fragment_addition
124
+ d = QDA::Document.new('The Title')
125
+ d.append('abc def ghi jkl')
126
+ d.append('ABC DEF GHI JKL')
127
+ fr_1 = d[0,4]
128
+ fr_2 = d[2,5]
129
+ fr_3 = d[12,6]
130
+
131
+ assert_equal('abc def', fr_1 + fr_2,
132
+ 'Fragment addition')
133
+ assert_equal( d[0, 14], fr_1 + d[3, 11],
134
+ 'Overlapping fragment addition')
135
+ assert_equal(['abc ', "jkl\nAB"], fr_1 + fr_3,
136
+ 'Disjoint fragment addition')
137
+ assert_equal('abc ', fr_1 + d[1,2],
138
+ 'Surrounded fragment addition')
139
+
140
+ cs_1 = QDA::CodeSet[ fr_1, fr_3 ]
141
+ cs_2 = QDA::CodeSet[ d[3, 11] ]
142
+
143
+ results = cs_1.union(cs_2)
144
+ assert_equal( [ d[0, 18 ] ], results,
145
+ 'CodeSet union')
146
+
147
+ results = cs_1.union(cs_2)
148
+ assert_equal( [ d[0, 18] ], results,
149
+ 'CodeSet union')
150
+ end
151
+
152
+ def test_fragment_intersect
153
+ d = QDA::Document.new('The Title')
154
+ d.append('abc def ghi jkl')
155
+ d.append('ABC DEF GHI JKL')
156
+ fr_1 = d[0,4]
157
+ fr_2 = d[2,5]
158
+ fr_3 = d[12,6]
159
+ assert_equal('c ', fr_1 % fr_2,
160
+ 'Fragment intersection')
161
+
162
+ assert_nil(fr_1 % fr_3,
163
+ 'Disjoint fragment intersection')
164
+
165
+ assert_equal('bc', fr_1 % d[1,2],
166
+ 'overlapping fragment intersection')
167
+ end
168
+
169
+ def test_fragment_exclude
170
+ d = QDA::Document.new('The Title')
171
+ d.append('abc def ghi jkl')
172
+ d.append('ABC DEF GHI JKL')
173
+ fr_1 = d[0,4]
174
+ fr_2 = d[2,5]
175
+ fr_3 = d[12,6]
176
+ assert_equal([ 'ab' ], fr_1 - fr_2,
177
+ 'Fragment exclusion')
178
+
179
+ assert_equal([ 'abc ' ], fr_1 - fr_3,
180
+ 'Disjoint fragment exclusion')
181
+
182
+ assert_equal([ 'a', ' ' ], fr_1 - d[1,2],
183
+ 'overlapping fragment exclusion')
184
+
185
+ cs_1 = QDA::CodeSet[ fr_1, fr_3 ]
186
+ cs_2 = QDA::CodeSet[ d[3, 11] ]
187
+
188
+ results = cs_1.exclude(cs_2)
189
+ assert_equal( [ d[0,3], d[14, 4] ], results,
190
+ 'CodeSet subtraction')
191
+
192
+ results = cs_1.exclude(cs_2)
193
+ assert_equal( [ d[0,3], d[14, 4] ], results,
194
+ 'CodeSet subtraction')
195
+ end
196
+ end