shift_subtitles 0.1.0

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.
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'ruby-debug'
4
+ require 'optparse'
5
+
6
+ require File.dirname(__FILE__) + '/../lib/shift_subtitles'
7
+
8
+ options = {}
9
+
10
+ optparse = OptionParser.new do |opts|
11
+ options[:action] = nil
12
+ opts.on('--action OPERATION') { |action| options[:action] = action }
13
+
14
+ options[:time] = nil
15
+ opts.on('--time TIME') { |time| options[:time] = time }
16
+
17
+ options[:input] = nil
18
+ opts.on('--input INPUT') { |input| options[:input] = input }
19
+
20
+ options[:output] = nil
21
+ opts.on('--output OUTPUT') { |output| options[:output] = output }
22
+ end
23
+
24
+ optparse.parse!
25
+
26
+ begin
27
+ ShiftSubtitles::Process.shift_subtitles(options)
28
+ rescue => e
29
+ puts "OOPS! something went wrong......#{e.message}"
30
+ else
31
+ puts 'Shift Subtitle successfully run. Welcome to AV heaven'
32
+ end
@@ -0,0 +1,11 @@
1
+ class Numeric
2
+
3
+ def minutes_to_secs
4
+ self*60
5
+ end
6
+
7
+ def hours_to_secs
8
+ self*60*60
9
+ end
10
+
11
+ end
@@ -0,0 +1,16 @@
1
+ module ShiftSubtitles
2
+
3
+ module FileHelper
4
+
5
+ def self.operation_with_validation operation, file_name
6
+ raise("Please specify an #{operation} file") unless file_name
7
+ if operation == 'input'
8
+ raise("Please specify a valid #{operation} file") unless File.exists?(file_name)
9
+ end
10
+ yield
11
+ end
12
+
13
+
14
+ end
15
+
16
+ end
@@ -0,0 +1,47 @@
1
+ module ShiftSubtitles
2
+
3
+ class Operation
4
+
5
+ attr_reader :action, :time
6
+
7
+ def initialize action, time
8
+ @action = action_with_validation(action)
9
+ @time = time_with_validation(time)
10
+ end
11
+
12
+ def seconds_difference
13
+ case action
14
+ when "add" then return time_difference
15
+ when "subtract" then return negative_time_difference
16
+ end
17
+ end
18
+
19
+ def action_with_validation action
20
+ valid_action?(action) ? action : raise("Please specify either 'add' or 'subtract' as the operation")
21
+ end
22
+
23
+ def time_with_validation time
24
+ raise("Please specify a time to shift") unless time
25
+ raise("Please specify a valid time to shift") unless valid_time?(time)
26
+ time
27
+ end
28
+
29
+ def valid_time? time
30
+ time =~ /\A[0-9]{2},[0-9]{3}\Z/
31
+ end
32
+
33
+ def valid_action? action
34
+ action =~ /\A(add|subtract)\Z/
35
+ end
36
+
37
+ def time_difference
38
+ time.sub(',','.').to_f
39
+ end
40
+
41
+ def negative_time_difference
42
+ -time_difference
43
+ end
44
+
45
+ end
46
+
47
+ end
@@ -0,0 +1,20 @@
1
+ module ShiftSubtitles
2
+
3
+ module Process
4
+
5
+ def self.shift_subtitles options
6
+ operation = ShiftSubtitles::Operation.new(options[:action], options[:time])
7
+ subtitles = ShiftSubtitles::Subtitles.new(options[:input])
8
+ subtitles.update_subtitles(operation.seconds_difference)
9
+ create_and_populate_output_file(options[:output], subtitles)
10
+ end
11
+
12
+ def self.create_and_populate_output_file output_file_name, subtitles
13
+ ShiftSubtitles::FileHelper.operation_with_validation('output', output_file_name) do
14
+ File.open(output_file_name, 'w') { |file| file << subtitles.formatted_subtitles_for_file }
15
+ end
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,36 @@
1
+ require 'time'
2
+
3
+ module ShiftSubtitles
4
+
5
+ class Subtitle
6
+
7
+ attr_reader :index, :start_time, :end_time, :text
8
+
9
+ def initialize subtitle_string
10
+ subtitle_pattern =~ subtitle_string
11
+ @index, @start_time, @end_time, @text = $1, $2, $3, $4
12
+ end
13
+
14
+ def update_duration seconds_difference
15
+ @start_time = update_time(start_time, seconds_difference)
16
+ @end_time = update_time(end_time, seconds_difference)
17
+ end
18
+
19
+ def update_time time_string, seconds_difference
20
+ time = Time.parse(time_string)
21
+ updated_time = Time.parse(time_string) + seconds_difference
22
+ raise("Please specify a legal time to shift, this will push the subs into negative values") if updated_time.day < time.day
23
+ updated_time.strftime("%H:%M:%S,%L")
24
+ end
25
+
26
+ def format
27
+ "#{index}\n#{start_time} --> #{end_time}\n#{text}\n\n"
28
+ end
29
+
30
+ def subtitle_pattern
31
+ /([0-9]+)\n([0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}) --> ([0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3})\n(.*)/
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,36 @@
1
+ module ShiftSubtitles
2
+
3
+ class Subtitles
4
+
5
+ include Enumerable
6
+
7
+ attr_accessor :subtitle_list
8
+
9
+ def initialize input_file_path
10
+ input_file_subtitles = input_file_contents(input_file_path).scan(input_file_pattern)
11
+ @subtitle_list = input_file_subtitles.collect { |subtitle| ShiftSubtitles::Subtitle.new(subtitle) }
12
+ end
13
+
14
+ def each
15
+ @subtitle_list.each {|subtitle| yield(subtitle)}
16
+ end
17
+
18
+ def update_subtitles seconds_difference
19
+ each { |subtitle| subtitle.update_duration(seconds_difference) }
20
+ end
21
+
22
+ def formatted_subtitles_for_file
23
+ collect { |subtitle| subtitle.format }.join
24
+ end
25
+
26
+ def input_file_contents input_file_path
27
+ ShiftSubtitles::FileHelper.operation_with_validation('input', input_file_path) { File.read(input_file_path) }
28
+ end
29
+
30
+ def input_file_pattern
31
+ /[0-9]+\n[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3} --> [0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\n.*/
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,6 @@
1
+ require File.dirname(__FILE__) + '/shift_subtitles/process'
2
+ require File.dirname(__FILE__) + '/shift_subtitles/operation'
3
+ require File.dirname(__FILE__) + '/shift_subtitles/subtitles'
4
+ require File.dirname(__FILE__) + '/shift_subtitles/subtitle'
5
+ require File.dirname(__FILE__) + '/shift_subtitles/file_helper'
6
+ require File.dirname(__FILE__) + '/shift_subtitles/core_ext/numeric'
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe Numeric do
4
+
5
+ it "converts a 1 minute into 60 seconds" do
6
+ 1.minutes_to_secs.should == 60
7
+ end
8
+
9
+ it "converts a 1 hour into 3600 seconds" do
10
+ 1.hours_to_secs.should == 3600
11
+ end
12
+
13
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe ShiftSubtitles::FileHelper do
4
+
5
+ before(:each) do
6
+ @block = lambda { 'yielded_value' }
7
+ end
8
+
9
+ context "output file" do
10
+
11
+ it "yields the block when a file_name & block are passed in" do
12
+ File.should_not_receive(:exists?)
13
+ yielded_value = ShiftSubtitles::FileHelper.operation_with_validation('output', 'filename.txt', &@block)
14
+ yielded_value.should == 'yielded_value'
15
+ end
16
+
17
+ it "raises a invalid file error when no file_name & block are passed in" do
18
+ File.should_not_receive(:exists?)
19
+ lambda { ShiftSubtitles::FileHelper.operation_with_validation('output', nil, &@block) }.should raise_error("Please specify an output file")
20
+ end
21
+
22
+ end
23
+
24
+ context "input file" do
25
+
26
+ it "yields the block when a file_name & block are passed in" do
27
+ File.should_receive(:exists?).with('filename.txt').and_return(true)
28
+ yielded_value = ShiftSubtitles::FileHelper.operation_with_validation('input', 'filename.txt', &@block)
29
+ yielded_value.should == 'yielded_value'
30
+ end
31
+
32
+ it "raises a invalid file error when no file_name is passed in" do
33
+ File.should_not_receive(:exists?)
34
+ lambda { ShiftSubtitles::FileHelper.operation_with_validation('input', nil, &@block) }.should raise_error("Please specify an input file")
35
+ end
36
+
37
+ it "raises a file doesn't exist error when an invalid file_name is passed in" do
38
+ File.should_receive(:exists?).with('invalid.txt').and_return(false)
39
+ lambda { ShiftSubtitles::FileHelper.operation_with_validation('input', 'invalid.txt', &@block) }.should raise_error("Please specify a valid input file")
40
+ end
41
+
42
+ end
43
+
44
+ end
45
+
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+
3
+ describe ShiftSubtitles::Operation do
4
+
5
+ context "operation adds 2 seconds" do
6
+
7
+ before(:each) do
8
+ @operation = ShiftSubtitles::Operation.new('add', '02,000')
9
+ end
10
+
11
+ it "sets the action on initialization" do
12
+ @operation.action.should == 'add'
13
+ end
14
+
15
+ it "sets the time on initialization" do
16
+ @operation.time.should == '02,000'
17
+ end
18
+
19
+ it "converts the time and action to a float of the amount of seconds to change" do
20
+ @operation.seconds_difference.should == 2.0
21
+ end
22
+
23
+ end
24
+
25
+ context "operation subtracts 1.5 seconds" do
26
+
27
+ before(:each) do
28
+ @operation = ShiftSubtitles::Operation.new('subtract', '01,500')
29
+ end
30
+
31
+ it "converts the time and action to a float of the amount of seconds to change" do
32
+ @operation.seconds_difference.should == -1.5
33
+ end
34
+
35
+ end
36
+
37
+ context "invalid scenarios" do
38
+
39
+ it "no time is passed in on initialization raises an error" do
40
+ lambda { @operation = ShiftSubtitles::Operation.new('subtract', nil) }.should raise_error("Please specify a time to shift")
41
+ end
42
+
43
+ it "invalid time is passed in on initialization raises an error" do
44
+ lambda { @operation = ShiftSubtitles::Operation.new('subtract', 'rogue-time') }.should raise_error("Please specify a valid time to shift")
45
+ end
46
+
47
+ it "invalid time is passed in on initialization raises an error" do
48
+ lambda { @operation = ShiftSubtitles::Operation.new('subtract', '111,889') }.should raise_error("Please specify a valid time to shift")
49
+ end
50
+
51
+ it "invalid time is passed in on initialization raises an error" do
52
+ lambda { @operation = ShiftSubtitles::Operation.new('ssubtractt', '02,222') }.should raise_error("Please specify either 'add' or 'subtract' as the operation")
53
+ end
54
+
55
+ end
56
+
57
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe ShiftSubtitles::Process do
4
+
5
+ context "valid input" do
6
+
7
+ it "reads in the output file name and creates the file" do
8
+ options = {:action => "add", :time => "2,000", :input => "input.srt", :output => "output.srt"}
9
+ mock_operation = mock(ShiftSubtitles::Operation, :seconds_difference => 2.0)
10
+ mock_subtitles = mock(ShiftSubtitles::Subtitles, :formatted_subtitles => 'formatted_subtitles')
11
+ ShiftSubtitles::Operation.should_receive(:new).with("add", "2,000").and_return(mock_operation)
12
+ ShiftSubtitles::Subtitles.should_receive(:new).with("input.srt").and_return(mock_subtitles)
13
+ file_block = lambda { File.open(options[:output], 'w') { |file| file << mock_subtitles.formatted_subtitles_for_file } }
14
+ ShiftSubtitles::FileHelper.should_receive(:operation_with_validation).with('output', options[:output], &file_block)
15
+ mock_subtitles.should_receive(:update_subtitles).with(2.0)
16
+ mock_operation.should_receive(:seconds_difference).with()
17
+ mock_subtitles.should_receive(:formatted_subtitles_for_file).with()
18
+ ShiftSubtitles::Process.shift_subtitles(options)
19
+ end
20
+
21
+ end
22
+
23
+ context "invalid input" do
24
+
25
+ it "fails when no output file is specified" do
26
+ options = {:action => "add", :time => "2,000", :input => "input.srt"}
27
+ mock_operation = mock(ShiftSubtitles::Operation, :seconds_difference => 2.0)
28
+ mock_subtitles = mock(ShiftSubtitles::Subtitles)
29
+ ShiftSubtitles::Operation.should_receive(:new).with("add", "2,000").and_return(mock_operation)
30
+ ShiftSubtitles::Subtitles.should_receive(:new).with("input.srt").and_return(mock_subtitles)
31
+ file_block = lambda { File.open(nil, 'w') { |file| file << mock_subtitles.formatted_subtitles_for_file } }
32
+ ShiftSubtitles::FileHelper.should_receive(:operation_with_validation).with('output', nil, &file_block)
33
+ mock_subtitles.should_receive(:update_subtitles).with(2.0)
34
+ mock_operation.should_receive(:seconds_difference).with()
35
+ mock_subtitles.should_not_receive(:formatted_subtitles_for_file)
36
+
37
+ lambda { ShiftSubtitles::Process.shift_subtitles(options) }.should raise_error
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+
3
+ describe ShiftSubtitles::Subtitle do
4
+
5
+ context "valid subtitle" do
6
+
7
+ before(:each) do
8
+ @subtitle = ShiftSubtitles::Subtitle.new("1\n01:31:51,210 --> 01:31:54,893\ntext")
9
+ end
10
+
11
+ it "sets an index on initialization" do
12
+ @subtitle.index.should == '1'
13
+ end
14
+
15
+ it "sets the start_time on initialization" do
16
+ @subtitle.start_time.should == '01:31:51,210'
17
+ end
18
+
19
+ it "sets the end_time on initialization" do
20
+ @subtitle.end_time.should == '01:31:54,893'
21
+ end
22
+
23
+ it "sets the text on initialization" do
24
+ @subtitle.text.should == 'text'
25
+ end
26
+
27
+ it "adds 2 seconds to the start_time and end_time" do
28
+ seconds_difference = 2.0
29
+ @subtitle.update_duration(seconds_difference)
30
+ @subtitle.start_time.should == '01:31:53,210'
31
+ @subtitle.end_time.should == '01:31:56,893'
32
+ end
33
+
34
+ it "subtracts 1.5 seconds to the start_time and end_time" do
35
+ seconds_difference = -1.5
36
+ @subtitle.update_duration(seconds_difference)
37
+ @subtitle.start_time.should == '01:31:49,710'
38
+ @subtitle.end_time.should == '01:31:53,393'
39
+ end
40
+
41
+ it "formats index, start_time, end_time, text for output" do
42
+ formatted_subtitle = @subtitle.format
43
+ formatted_subtitle.should == "1\n01:31:51,210 --> 01:31:54,893\ntext\n\n"
44
+ end
45
+
46
+ end
47
+
48
+ context "valid subtitle with illegal seconds difference" do
49
+
50
+ it "raises an error for negative times" do
51
+ @subtitle = ShiftSubtitles::Subtitle.new("1\n00:00:01,210 --> 00:00:01,893\ntext")
52
+ seconds_difference = -3.0
53
+ lambda { @subtitle.update_duration(seconds_difference) }.should raise_error("Please specify a legal time to shift, this will push the subs into negative values")
54
+ end
55
+
56
+ end
57
+
58
+ end
@@ -0,0 +1,72 @@
1
+ require 'spec_helper'
2
+
3
+ describe ShiftSubtitles::Subtitles do
4
+
5
+ context "a file with 1 subtitle is input" do
6
+
7
+ before(:each) do
8
+ input_file_contents = "1\n01:31:51,210 --> 01:31:54,893\ntext"
9
+ @mock_subtitle = mock(ShiftSubtitles::Subtitle)
10
+ @seconds_difference = 2.0
11
+ File.should_receive(:read).with('input.srt').and_return(input_file_contents)
12
+ file_block = lambda { File.read('input.srt') }
13
+ ShiftSubtitles::FileHelper.should_receive(:operation_with_validation).with('input', 'input.srt', &file_block)
14
+ ShiftSubtitles::Subtitle.should_receive(:new).once.with("1\n01:31:51,210 --> 01:31:54,893\ntext").and_return(@mock_subtitle)
15
+ @subtitles = ShiftSubtitles::Subtitles.new('input.srt')
16
+ end
17
+
18
+ it "is initialized with a file containing 1 subtitle" do
19
+ @subtitles.subtitle_list.count.should == 1
20
+ end
21
+
22
+ it "includes the Enumerable each method to access the subtitle_list" do
23
+ @subtitles.count.should == 1
24
+ end
25
+
26
+ it "updates each subtitle using the Operation" do
27
+ @mock_subtitle.should_receive(:update_duration).with(@seconds_difference)
28
+ updated_subtitles = @subtitles.update_subtitles(@seconds_difference)
29
+ end
30
+
31
+ it "formats the subtitles for output" do
32
+ @mock_subtitle.should_receive(:format).once.and_return("1\n01:31:51,210 --> 01:31:54,893\ntext\n\n")
33
+ formatted_subtitles = @subtitles.formatted_subtitles_for_file
34
+ formatted_subtitles.should == "1\n01:31:51,210 --> 01:31:54,893\ntext\n\n"
35
+ end
36
+
37
+ end
38
+
39
+ context "a file with 2 subtitles is input" do
40
+
41
+ before(:each) do
42
+ input_file_contents = "1\n01:31:51,210 --> 01:31:54,893\ntext\n\n2\n01:31:54,210 --> 01:31:57,893\nmore text\n\n\n"
43
+ @mock_subtitle = mock(ShiftSubtitles::Subtitle)
44
+ @seconds_difference = 2.0
45
+ File.should_receive(:read).with('input.srt').and_return(input_file_contents)
46
+ file_block = lambda { File.read('input.srt') }
47
+ ShiftSubtitles::FileHelper.should_receive(:operation_with_validation).with('input', 'input.srt', &file_block)
48
+ ShiftSubtitles::Subtitle.should_receive(:new).once.with("1\n01:31:51,210 --> 01:31:54,893\ntext").and_return(@mock_subtitle)
49
+ ShiftSubtitles::Subtitle.should_receive(:new).once.with("2\n01:31:54,210 --> 01:31:57,893\nmore text").and_return(@mock_subtitle)
50
+ @subtitles = ShiftSubtitles::Subtitles.new('input.srt')
51
+ end
52
+
53
+ it "is initialized with a file containing 2 subtitles" do
54
+ @subtitles.subtitle_list.count.should == 2
55
+ end
56
+
57
+ end
58
+
59
+ context "invalid input file" do
60
+
61
+ it "is initialized with no file" do
62
+ ShiftSubtitles::FileHelper.should_receive(:operation_with_validation).and_raise
63
+ lambda { ShiftSubtitles::Subtitles.new(nil) }.should raise_error
64
+ end
65
+
66
+ it "is initialized with an invalid filename" do
67
+ ShiftSubtitles::FileHelper.should_receive(:operation_with_validation).and_raise
68
+ lambda { ShiftSubtitles::Subtitles.new('dud') }.should raise_error
69
+ end
70
+ end
71
+
72
+ end
@@ -0,0 +1,6 @@
1
+ require File.dirname(__FILE__) + '/../lib/shift_subtitles'
2
+
3
+ RSpec.configure do |config|
4
+ config.treat_symbols_as_metadata_keys_with_true_values = true
5
+ config.color_enabled = true
6
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shift_subtitles
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.0
6
+ platform: ruby
7
+ authors:
8
+ - James Shipton
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-10-31 00:00:00 +00:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: rspec
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ~>
23
+ - !ruby/object:Gem::Version
24
+ version: "2.6"
25
+ type: :development
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: cucumber
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ~>
34
+ - !ruby/object:Gem::Version
35
+ version: 1.1.0
36
+ type: :development
37
+ version_requirements: *id002
38
+ - !ruby/object:Gem::Dependency
39
+ name: aruba
40
+ prerelease: false
41
+ requirement: &id003 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ version: 0.4.6
47
+ type: :development
48
+ version_requirements: *id003
49
+ description: "Shift Subtitles - Ruby Learning Challenge #1"
50
+ email:
51
+ - ionysis@gmail.com
52
+ executables:
53
+ - shift_subtitles
54
+ extensions: []
55
+
56
+ extra_rdoc_files: []
57
+
58
+ files:
59
+ - lib/shift_subtitles/core_ext/numeric.rb
60
+ - lib/shift_subtitles/file_helper.rb
61
+ - lib/shift_subtitles/operation.rb
62
+ - lib/shift_subtitles/process.rb
63
+ - lib/shift_subtitles/subtitle.rb
64
+ - lib/shift_subtitles/subtitles.rb
65
+ - lib/shift_subtitles.rb
66
+ - spec/shift_subtitles/core_ext/numeric_spec.rb
67
+ - spec/shift_subtitles/file_helper_spec.rb
68
+ - spec/shift_subtitles/operation_spec.rb
69
+ - spec/shift_subtitles/process_spec.rb
70
+ - spec/shift_subtitles/subtitle_spec.rb
71
+ - spec/shift_subtitles/subtitles_spec.rb
72
+ - spec/spec_helper.rb
73
+ - bin/shift_subtitles
74
+ has_rdoc: true
75
+ homepage: https://github.com/jamesshipton/quizzes/shift_subtitles
76
+ licenses: []
77
+
78
+ post_install_message:
79
+ rdoc_options: []
80
+
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: "0"
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: "0"
95
+ requirements: []
96
+
97
+ rubyforge_project:
98
+ rubygems_version: 1.6.2
99
+ signing_key:
100
+ specification_version: 3
101
+ summary: "Shift Subtitles - Ruby Learning Challenge #1"
102
+ test_files:
103
+ - spec/shift_subtitles/core_ext/numeric_spec.rb
104
+ - spec/shift_subtitles/file_helper_spec.rb
105
+ - spec/shift_subtitles/operation_spec.rb
106
+ - spec/shift_subtitles/process_spec.rb
107
+ - spec/shift_subtitles/subtitle_spec.rb
108
+ - spec/shift_subtitles/subtitles_spec.rb
109
+ - spec/spec_helper.rb