weft-qda 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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