mesh-medical-subject-headings 1.2.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/test/mesh_test.rb CHANGED
@@ -58,6 +58,20 @@ module MESH
58
58
  assert_includes mh.tree_numbers, 'C20.111.163'
59
59
  end
60
60
 
61
+ it 'should have the correct root letters' do
62
+ mh = MESH::Mesh.find('D000224')
63
+ assert_equal ['C'], mh.roots
64
+ mh = MESH::Mesh.find('D064946')
65
+ assert_equal ['H', 'N'], mh.roots
66
+ end
67
+
68
+ it 'should have the correct descriptor class' do
69
+ mh = MESH::Mesh.find('D000224')
70
+ assert_equal :topical_descriptor, mh.descriptor_class
71
+ mh = MESH::Mesh.find('D005260')
72
+ assert_equal :check_tag, mh.descriptor_class
73
+ end
74
+
61
75
  it 'should have the correct original heading' do
62
76
  mh = MESH::Mesh.find('D000224')
63
77
  assert_equal 'Addison Disease', mh.original_heading
@@ -141,6 +155,10 @@ module MESH
141
155
  assert_includes parent.children, child4
142
156
  end
143
157
 
158
+ it 'should have the correct siblings' do
159
+ skip
160
+ end
161
+
144
162
  it 'should match on conditions for original_heading' do
145
163
  mh = MESH::Mesh.find('D001471')
146
164
  assert mh.matches(original_heading: /^Barrett Esophagus$/)
@@ -161,6 +179,29 @@ module MESH
161
179
  refute mh.matches(entries: /Foo/)
162
180
  end
163
181
 
182
+ it 'should match on descriptor class' do
183
+ mh = MESH::Mesh.find('D000224')
184
+ assert mh.matches(descriptor_class: :topical_descriptor)
185
+ refute mh.matches(descriptor_class: :check_tag)
186
+ mh = MESH::Mesh.find('D005260')
187
+ assert mh.matches(descriptor_class: :check_tag)
188
+ refute mh.matches(descriptor_class: :topical_descriptor)
189
+ end
190
+
191
+ it 'should match on conditions for tree numbers' do
192
+ mh = MESH::Mesh.find('D001471')
193
+ assert mh.matches(tree_numbers: /C06\.198\.102/)
194
+ assert mh.matches(tree_numbers: /^C06\.198\.102$/)
195
+ assert mh.matches(tree_numbers: /^C06/)
196
+ assert mh.matches(tree_numbers: /\.198\./)
197
+ assert mh.matches(tree_numbers: /^C06\.405\.117\.102$/)
198
+ end
199
+
200
+ it 'should not match on incorrect conditions for tree numbers' do
201
+ mh = MESH::Mesh.find('D001471')
202
+ refute mh.matches(tree_numbers: /Foo/)
203
+ end
204
+
164
205
  it 'should match on conditions for summary' do
165
206
  mh = MESH::Mesh.find('D001471')
166
207
  assert mh.matches(summary: /the lower ESOPHAGUS resulting from chronic acid reflux \(ESOPHAGITIS, REFLUX\)\./)
@@ -197,15 +238,26 @@ module MESH
197
238
  end
198
239
 
199
240
  it 'should match headings that occur in given text' do
200
- expected_ids = %w(D001491 D001769 D001792 D001842 D001853 D002470 D002477 D002648 D002875 D002965 D002999 D003062 D003561 D003593 D003643 D004194 D004314 D004813 D004912 D005091 D005123 D005293 D005333 D005385 D005544 D005796 D006128 D006225 D006309 D006321 D006331 D006664 D007107 D007223 D007231 D007239 D007246 D007938 D008099 D008168 D008214 D008423 D008533 D008607 D008722 D009035 D009055 D009132 D009154 D009190 D009196 D009369 D009666 D010372 D010641 D011153 D012008 D012106 D012146 D012306 D012307 D012380 D012680 D012867 D013534 D013577 D013601 D013812 D013921 D013961 D014034 D014157 D014171 D014314 D014960 D015032 D015994 D015995 D016424 D016433 D017584 D017668 D018387 D018388 D019021 D019070 D019368 D019369 D032882 D036801 D038042 D041905 D052016 D055016)
241
+ expected_ids = %w(D001491 D001769 D001792 D001853 D002470 D002477 D002648 D002965 D002999 D003561 D003593 D003643 D004194 D004314 D004813 D004912 D005091 D005123 D005293 D005333 D005385 D005544 D005796 D006128 D006225 D006309 D006321 D006331 D006405 D007107 D007223 D007231 D007239 D007246 D007938 D007947 D008099 D008168 D008214 D008423 D008533 D008607 D008722 D009035 D009055 D009132 D009154 D009190 D009196 D009369 D009666 D010372 D010641 D011153 D012008 D012106 D012146 D012306 D012307 D012380 D012680 D012867 D013534 D013601 D013812 D013921 D013961 D014034 D014157 D014171 D014960 D015032 D015470 D015994 D015995 D016424 D016433 D017584 D017668 D018387 D018388 D019021 D019070 D019368 D019369 D032882 D036801 D038042 D041905 D052016 D054198 D055016)
201
242
  expected = expected_ids.map { |id| MESH::Mesh.find(id) }
202
243
  matches = MESH::Mesh.match_in_text(@example_text)
203
244
  actual = matches.map { |match| match[:heading] }.uniq
204
- assert_equal expected, actual
245
+ assert_equal expected.sort, actual.sort
246
+ end
247
+
248
+ it 'should sort based on unique id' do
249
+ skip
250
+ end
251
+
252
+ it 'should only match the most specific matches in given text' do
253
+ expected = MESH::Mesh.find('D054144')
254
+ actual = MESH::Mesh.match_in_text('Diastolic Heart Failure')
255
+ assert_equal 1, actual.length
256
+ assert_equal expected, actual.first[:heading]
205
257
  end
206
258
 
207
259
  it 'should only match useful headings that occur in given text' do
208
- expected_ids = %w(D001491 D001769 D001792 D001842 D001853 D002470 D002648 D002875 D002965 D003062 D003561 D003593 D003643 D004194 D004314 D004813 D004912 D005091 D005123 D005293 D005333 D005385 D005544 D005796 D006128 D006225 D006309 D006321 D006331 D007107 D007231 D007239 D007938 D008099 D008168 D008214 D008423 D008607 D008722 D009035 D009055 D009132 D009154 D009190 D009196 D009369 D009666 D010372 D010641 D011153 D012008 D012106 D012146 D012306 D012307 D012380 D012680 D012867 D013534 D013577 D013601 D013812 D013921 D013961 D014034 D014157 D014171 D014314 D015032 D015994 D015995 D016424 D017584 D017668 D018387 D018388 D019021 D019070 D019368 D019369 D032882 D036801 D038042 D041905 D052016)
260
+ expected_ids = %w(D001491 D001769 D001792 D001853 D002470 D002648 D002875 D002965 D003561 D003593 D003643 D004194 D004314 D004813 D004912 D005091 D005123 D005293 D005333 D005385 D005544 D005796 D006128 D006225 D006309 D006321 D006331 D006405 D007107 D007231 D007239 D007938 D007947 D008099 D008168 D008214 D008423 D008607 D008722 D009035 D009055 D009132 D009154 D009190 D009196 D009369 D009666 D010372 D010641 D011153 D012008 D012106 D012146 D012306 D012307 D012380 D012680 D012867 D013534 D013601 D013812 D013921 D013961 D014034 D014157 D014171 D015032 D015470 D015994 D015995 D016424 D017584 D017668 D018387 D018388 D019021 D019070 D019368 D019369 D032882 D036801 D038042 D041905 D052016 D054198)
209
261
 
210
262
  not_useful_ids = %w(D007246 D002477 D014960 D008533 D016433 D006664 D055016 D002999 D007223)
211
263
  begin
@@ -234,6 +286,26 @@ module MESH
234
286
  assert_equal MESH::Mesh.find('D007938'), matches[0][:heading]
235
287
  end
236
288
 
289
+ it 'should return no matches when given nil text' do
290
+ assert_equal [], MESH::Mesh.match_in_text(nil)
291
+ end
292
+
293
+ it 'should only match uppercase entries with uppercase text' do
294
+ text = 'Lorem amet, consectetur adipiscing elit. Donec pretium ATP leo diam, quis adipiscing purus bibendum.'
295
+ matches = MESH::Mesh.match_in_text(text)
296
+ assert_equal 1, matches.length
297
+ assert_equal MESH::Mesh.find('D000255'), matches[0][:heading]
298
+ text = 'Lorem ipsum consectetur adipiscing elit. Donec pretium atp leo diam, quis adipiscing purus bibendum.'
299
+ assert_equal [], MESH::Mesh.match_in_text(text)
300
+ end
301
+
302
+ it 'should match anglicised terms in text' do
303
+ text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec pretium leo diam, quis adipiscing purus bibendum eu leukaemia'
304
+ matches = MESH::Mesh.match_in_text(text)
305
+ assert_equal 1, matches.length
306
+ assert_equal MESH::Mesh.find('D007938'), matches[0][:heading]
307
+ end
308
+
237
309
  it 'should allow headings to be marked as not useful' do
238
310
  mh = MESH::Mesh.find('D055550')
239
311
  mh.useful = true
@@ -244,9 +316,29 @@ module MESH
244
316
  assert mh.useful
245
317
  end
246
318
 
319
+ it 'should allow headings to be found with a where() match on original_heading' do
320
+ expected = [MESH::Mesh.find('D003561'), MESH::Mesh.find('D016238')]
321
+ actual = MESH::Mesh.where(original_heading: /^Cyta/)
322
+ assert_equal expected, actual
323
+ end
324
+
325
+ it 'should match on entries in where()' do
326
+ expected_ids = %w( D002397 D003064 D003400 D003532 D004284 D004289 D004555 D005412 D006054 D006196 D007059 D007497 D007695 D009990 D010473 D012091 D012487 D012758 D013215 D015027 D020410 D023721 D023761 D023781 D024541 D029961 D037401 D037462 D048251 D049832 D052656 D057096 )
327
+ expected = expected_ids.map { |id| MESH::Mesh.find(id) }
328
+ actual = MESH::Mesh.where(entries: /fish/)
329
+ assert_equal expected, actual
330
+ end
331
+
332
+ it 'should match on tree numbers in where()' do
333
+ expected_ids = %w( D000005 D001415 D010388 D013909 )
334
+ expected = expected_ids.map { |id| MESH::Mesh.find(id) }
335
+ actual = MESH::Mesh.where(tree_numbers: /^A01\.923\.[0-9]{3}$/)
336
+ assert_equal expected, actual
337
+ end
338
+
247
339
  it 'should match on useful in where' do
340
+ expected = [MESH::Mesh.find('D012000'), MESH::Mesh.find('D064906'), MESH::Mesh.find('D064966')]
248
341
  begin
249
- expected = [MESH::Mesh.find('D012000'), MESH::Mesh.find('D064906'), MESH::Mesh.find('D064966')]
250
342
  expected.each { |mh| mh.useful = false }
251
343
  actual = MESH::Mesh.where(useful: false)
252
344
  assert_equal expected, actual
@@ -255,6 +347,17 @@ module MESH
255
347
  end
256
348
  end
257
349
 
350
+ it 'should match on descriptor class in where' do
351
+ expected = [MESH::Mesh.find('D012000'), MESH::Mesh.find('D064906'), MESH::Mesh.find('D064966')]
352
+ begin
353
+ expected.each { |mh| mh.descriptor_class = :foo }
354
+ actual = MESH::Mesh.where(descriptor_class: :foo)
355
+ assert_equal expected, actual
356
+ ensure
357
+ expected.each { |mh| mh.descriptor_class = :topical_descriptor }
358
+ end
359
+ end
360
+
258
361
  it 'should only include useful headings in .each' do
259
362
  begin
260
363
  MESH::Mesh.each do |mh|
@@ -276,19 +379,75 @@ module MESH
276
379
  end
277
380
  end
278
381
 
382
+ it 'should know its deepest position in the tree' do
383
+ #single tree numbers
384
+ assert_equal 1, MESH::Mesh.find('D002319').deepest_position
385
+ assert_equal 2, MESH::Mesh.find('D001808').deepest_position
386
+ assert_equal 3, MESH::Mesh.find('D001158').deepest_position
387
+ assert_equal 4, MESH::Mesh.find('D001981').deepest_position
388
+ end
389
+
390
+ it 'should know its shallowest position in the tree' do
391
+ #single tree numbers
392
+ assert_equal 1, MESH::Mesh.find('D002319').shallowest_position
393
+ assert_equal 2, MESH::Mesh.find('D001808').shallowest_position
394
+ assert_equal 3, MESH::Mesh.find('D001158').shallowest_position
395
+ assert_equal 4, MESH::Mesh.find('D001981').shallowest_position
396
+ end
397
+
398
+ it 'should know if one heading is the descendant of another' do
399
+ parent = MESH::Mesh.find('D002319')
400
+ child = MESH::Mesh.find('D001808')
401
+ grandchild = MESH::Mesh.find('D001158')
402
+ great_grandchild = MESH::Mesh.find('D001981')
403
+ unrelated = MESH::Mesh.find('D008091')
404
+
405
+ refute parent.has_descendant(parent), 'should not consider itself a desecendant'
406
+ assert parent.has_descendant(child), "should consider child #{child.inspect} a descendant of #{parent.inspect} in #{parent.children}"
407
+ assert parent.has_descendant(grandchild), "should consider grandchild #{grandchild.inspect} a descendant #{parent.inspect}"
408
+ assert parent.has_descendant(great_grandchild), "should consider great grandchild #{great_grandchild.inspect} a descendant #{parent.inspect}"
409
+ refute parent.has_descendant(unrelated), 'should not consider an unrelated heading a descendant'
410
+ end
411
+
412
+ it 'should know if one heading is the ancestor of another' do
413
+ child = MESH::Mesh.find('D001981')
414
+ parent = MESH::Mesh.find('D001158')
415
+ grandparent = MESH::Mesh.find('D001808')
416
+ great_grandparent = MESH::Mesh.find('D002319')
417
+ unrelated = MESH::Mesh.find('D008091')
418
+
419
+ refute child.has_ancestor(child), 'should not consider itself an ancestor'
420
+ assert child.has_ancestor(parent), "should consider parent #{parent.inspect} an ancestor of #{child.inspect} in #{child.parents}"
421
+ assert child.has_ancestor(grandparent), "should consider grandparent #{grandparent.inspect} an ancestor #{child.inspect}"
422
+ assert child.has_ancestor(great_grandparent), "should consider great grandparent #{great_grandparent.inspect} an ancestor #{child.inspect}"
423
+ refute child.has_ancestor(unrelated), 'should not consider an unrelated heading an ancestor'
424
+ end
425
+
426
+ it 'should know if headings are siblings at the same level below a common parent' do
427
+ parent = MESH::Mesh.find_by_tree_number('C19.053.500')
428
+ child1 = MESH::Mesh.find_by_tree_number('C19.053.500.263')
429
+ child2 = MESH::Mesh.find_by_tree_number('C19.053.500.270')
430
+ child3 = MESH::Mesh.find_by_tree_number('C19.053.500.480')
431
+ child4 = MESH::Mesh.find_by_tree_number('C19.053.500.740')
432
+ unrelated = MESH::Mesh.find('D008091')
433
+ children = [child1, child2, child3, child4]
434
+
435
+ children.each { |c| refute parent.sibling?(c) }
436
+ children.each { |c| refute c.sibling?(parent) }
437
+
438
+ children.each { |c| assert child1.sibling?(c) unless c == child1 }
439
+ children.each { |c| assert c.sibling?(child1) unless c == child1 }
440
+
441
+ children.each { |c| refute unrelated.sibling?(c) }
442
+ children.each { |c| refute c.sibling?(unrelated) }
443
+ end
444
+
279
445
  it 'should override inspect to prevent issues in test diagnostics' do
280
446
  mh = MESH::Mesh.find('D001471')
281
- expected = "#{mh.unique_id}, #{mh.original_heading}"
447
+ expected = "#{mh.unique_id}, #{mh.original_heading}, [#{mh.tree_numbers.join(',')}]"
282
448
  assert_equal expected, mh.inspect
283
449
  end
284
450
 
285
- it 'should allow headings to be found with a where() match on original_heading' do
286
- expected = [MESH::Mesh.find('D003561'), MESH::Mesh.find('D016238')]
287
- actual = MESH::Mesh.where(original_heading: /^Cyta/)
288
- assert_equal expected, actual
289
- end
290
- #MESH::Mesh.where(:entries, /.*Fish.*/)
291
-
292
451
  before do
293
452
 
294
453
  @example_text = 'Leukaemia in Downs Syndrome
data/test/test_helper.rb CHANGED
@@ -9,8 +9,17 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
9
9
 
10
10
  require "MESH"
11
11
 
12
+ puts 'Configuring MESH::Mesh — this may take up to 10 seconds.'
12
13
  start = Time.now
13
14
  MESH::Mesh.configure(filename: File.expand_path('../../data/mesh_data_2014/d2014.bin.gz', __FILE__))
14
15
  finish = Time.now
15
16
  configuration_time = finish - start
16
- raise 'MESH::Mesh should configure in less than 10 seconds.' unless configuration_time < 10
17
+ #raise 'MESH::Mesh should configure in less than 10 seconds.' unless configuration_time < 10
18
+
19
+ puts 'Translating MESH::Mesh into English ;) — this may take up to 60 seconds.'
20
+ start = Time.now
21
+ MESH::Mesh.translate('en-GB', MESH::Translator.new(MESH::Translator.enus_to_engb))
22
+ finish = Time.now
23
+ configuration_time = finish - start
24
+ puts "took #{configuration_time}"
25
+ #raise 'MESH::Mesh should translate in less than 30 seconds.' unless configuration_time < 60
@@ -5,14 +5,18 @@ module MESH
5
5
  describe 'Testing MESH:Translator core functions' do
6
6
 
7
7
  it 'should translate a single word' do
8
- tr = MESH::Translator.new
8
+ tr = MESH::Translator.new(MESH::Translator.enus_to_engb)
9
9
  assert_equal 'oesophagus', tr.translate('esophagus')
10
10
  assert_equal 'aluminium', tr.translate('aluminum')
11
11
  assert_equal 'gynaecology', tr.translate('gynecology')
12
+ tr = MESH::Translator.new(MESH::Translator.engb_to_enus)
13
+ assert_equal 'esophagus', tr.translate('oesophagus')
14
+ assert_equal 'aluminum', tr.translate('aluminium')
15
+ assert_equal 'gynecology', tr.translate('gynaecology')
12
16
  end
13
17
 
14
18
  it 'should translate within a body of text' do
15
- tr = MESH::Translator.new
19
+ tr = MESH::Translator.new(MESH::Translator.enus_to_engb)
16
20
  input = 'a condition with damage to the lining of the lower esophagus resulting from chronic acid reflux (esophagitis, reflux). through the process of metaplasia, the squamous cells are replaced by a columnar epithelium with cells resembling those of the intestine or the salmon-pink mucosa of the stomach. barrett\'s columnar epithelium is a marker for severe reflux and precursor to adenocarcinoma of the esophagus.'
17
21
  expected = 'a condition with damage to the lining of the lower oesophagus resulting from chronic acid reflux (oesophagitis, reflux). through the process of metaplasia, the squamous cells are replaced by a columnar epithelium with cells resembling those of the intestine or the salmon-pink mucosa of the stomach. barrett\'s columnar epithelium is a marker for severe reflux and precursor to adenocarcinoma of the oesophagus.'
18
22
 
@@ -20,28 +24,28 @@ module MESH
20
24
  end
21
25
 
22
26
  it 'should match uppercase' do
23
- tr = MESH::Translator.new
27
+ tr = MESH::Translator.new(MESH::Translator.enus_to_engb)
24
28
  input = 'A condition with damage to the lining of the lower ESOPHAGUS resulting from chronic acid reflux (ESOPHAGITIS, REFLUX). Through the process of metaplasia, the squamous cells are replaced by a columnar epithelium with cells resembling those of the INTESTINE or the salmon-pink mucosa of the STOMACH. Barrett\'s columnar epithelium is a marker for severe reflux and precursor to ADENOCARCINOMA of the esophagus.'
25
29
  expected = 'A condition with damage to the lining of the lower OESOPHAGUS resulting from chronic acid reflux (OESOPHAGITIS, REFLUX). Through the process of metaplasia, the squamous cells are replaced by a columnar epithelium with cells resembling those of the INTESTINE or the salmon-pink mucosa of the STOMACH. Barrett\'s columnar epithelium is a marker for severe reflux and precursor to ADENOCARCINOMA of the oesophagus.'
26
30
  assert_equal expected, tr.translate(input)
27
31
  end
28
32
 
29
33
  it 'should match title case' do
30
- tr = MESH::Translator.new
34
+ tr = MESH::Translator.new(MESH::Translator.enus_to_engb)
31
35
  input = 'A condition with damage to the lining of the lower Esophagus resulting from chronic acid reflux (Esophagitis, REFLUX). Through the process of metaplasia, the squamous cells are replaced by a columnar epithelium with cells resembling those of the INTESTINE or the salmon-pink mucosa of the STOMACH. Barrett\'s columnar epithelium is a marker for severe reflux and precursor to ADENOCARCINOMA of the esophagus.'
32
36
  expected = 'A condition with damage to the lining of the lower Oesophagus resulting from chronic acid reflux (Oesophagitis, REFLUX). Through the process of metaplasia, the squamous cells are replaced by a columnar epithelium with cells resembling those of the INTESTINE or the salmon-pink mucosa of the STOMACH. Barrett\'s columnar epithelium is a marker for severe reflux and precursor to ADENOCARCINOMA of the oesophagus.'
33
37
  assert_equal expected, tr.translate(input)
34
38
  end
35
39
 
36
40
  it 'should not change the input string' do
37
- tr = MESH::Translator.new
41
+ tr = MESH::Translator.new(MESH::Translator.enus_to_engb)
38
42
  input = 'esophagus'
39
43
  assert_equal 'oesophagus', tr.translate(input)
40
44
  assert_equal 'esophagus', input
41
45
  end
42
46
 
43
47
  it 'should maintain punctuation' do
44
- tr = MESH::Translator.new
48
+ tr = MESH::Translator.new(MESH::Translator.enus_to_engb)
45
49
  input = 'Esophagus, Barrett'
46
50
  assert_equal 'Oesophagus, Barrett', tr.translate(input)
47
51
  end
data/tr_speed.rb ADDED
@@ -0,0 +1,8 @@
1
+ require_relative 'lib/MESH'
2
+
3
+ puts DateTime.now
4
+ MESH::Mesh.configure(filename: 'data/mesh_data_2014/d2014.bin.gz')
5
+ puts DateTime.now
6
+ MESH::Mesh.translate('en-GB', MESH::Translator.new(MESH::Translator.enus_to_engb))
7
+ puts DateTime.now
8
+
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mesh-medical-subject-headings
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
- - mmmmmrob
7
+ - Rob Styles
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-03-05 00:00:00.000000000 Z
11
+ date: 2014-04-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -117,13 +117,16 @@ files:
117
117
  - data/mesh_data_2014/q2014.bin.gz
118
118
  - data/mesh_data_2014/useful_2014.tsv
119
119
  - lib/MESH.rb
120
+ - lib/MESH/classifier.rb
120
121
  - lib/MESH/mesh.rb
121
122
  - lib/MESH/translator.rb
122
123
  - lib/MESH/version.rb
123
124
  - match.rb
125
+ - test/classifier_test.rb
124
126
  - test/mesh_test.rb
125
127
  - test/test_helper.rb
126
128
  - test/translator_test.rb
129
+ - tr_speed.rb
127
130
  homepage: ''
128
131
  licenses:
129
132
  - AGPL3
@@ -150,6 +153,7 @@ specification_version: 4
150
153
  summary: A ruby gem containing MeSH subject headings (https://www.nlm.nih.gov/mesh/)
151
154
  for use in classifying and entity recognition.
152
155
  test_files:
156
+ - test/classifier_test.rb
153
157
  - test/mesh_test.rb
154
158
  - test/test_helper.rb
155
159
  - test/translator_test.rb