ztodo 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ coverage
5
+ doc/
6
+ lib/bundler/man
7
+ pkg/*
8
+ rdoc
9
+ spec/reports
10
+ tmp/*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ztodo.gemspec
4
+ gemspec
5
+
6
+
7
+ gem 'rspec'
8
+ gem 'colorize'
data/Gemfile.lock ADDED
@@ -0,0 +1,26 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ztodo (1.0.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ colorize (0.5.8)
10
+ diff-lcs (1.1.3)
11
+ rspec (2.12.0)
12
+ rspec-core (~> 2.12.0)
13
+ rspec-expectations (~> 2.12.0)
14
+ rspec-mocks (~> 2.12.0)
15
+ rspec-core (2.12.0)
16
+ rspec-expectations (2.12.0)
17
+ diff-lcs (~> 1.1.3)
18
+ rspec-mocks (2.12.0)
19
+
20
+ PLATFORMS
21
+ ruby
22
+
23
+ DEPENDENCIES
24
+ colorize
25
+ rspec
26
+ ztodo!
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Andreas Sotnik <andreas.sotnik@gmail.com>
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # ztodo
2
+
3
+ Simple Console Task Manager.
4
+
5
+ ## Installation
6
+
7
+ Install gem:
8
+
9
+ $ gem install ztodo
10
+
11
+ ## Basic Usage
12
+
13
+ Initializing ztodo project in current folder:
14
+
15
+ $ ztodo init
16
+
17
+ List of uncompleted tasks (or all tasks in project):
18
+
19
+ $ ztodo list [all]
20
+
21
+ Add task to project (%key% determines task in project, unique identifier):
22
+
23
+ $ ztodo add
24
+
25
+ Modify task:
26
+
27
+ $ ztodo modify %key%
28
+
29
+ Remove task:
30
+
31
+ $ ztodo remove %key%
32
+
33
+ You may call ztodo without parameters for getting help and information about current project:
34
+
35
+ $ ztodo
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new('spec')
5
+ task :default => :spec
data/bin/ztodo.rb ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require 'ztodo'
3
+
4
+ c = Ztodo::Controller.new
5
+ c.execute! ARGV
@@ -0,0 +1,158 @@
1
+ require 'colorize'
2
+ =begin
3
+ Class for processing user commands.
4
+ This class uses Ztodo::Project and Ztodo::Converter classes
5
+ =end
6
+ class Ztodo::Controller
7
+
8
+ def initialize
9
+ @proj = Ztodo::Project.new
10
+ @conv = Ztodo::Converter.new
11
+ end
12
+
13
+ # Execute necessary command
14
+ def execute! args
15
+ return cmd_help if args.empty?
16
+
17
+ handler = args[0]
18
+
19
+ if methods.include? ('cmd_'+handler).to_sym
20
+ args.slice! 0
21
+ method(('cmd_'+handler).to_sym).call args
22
+ else
23
+ cmd_help
24
+ end
25
+ end
26
+
27
+ protected
28
+
29
+ # Print help message
30
+ def cmd_help params=[]
31
+ puts 'ztodo - Simple Command Line Task Manager'.light_blue
32
+ puts 'Usage:'
33
+
34
+ puts '> ztodo'.green
35
+ puts '> ztodo help'.green
36
+ puts '> ztodo init'.yellow
37
+ puts '> ztodo init hard'.yellow
38
+ puts '> ztodo list'.green
39
+ puts '> ztodo list all'.green
40
+ puts '> ztodo add'.yellow
41
+ puts '> ztodo add %task-key%'.yellow
42
+ puts '> ztodo modify %task-key%'.yellow
43
+ puts '> ztodo remove %task-key%'.yellow
44
+ end
45
+
46
+ def cmd_init params=[]
47
+ if File.exists?(Dir.pwd+'/.ztodo') && params[0] != 'hard'
48
+ puts "ztodo project already existed. Run 'ztodo init hard' for re-init".red
49
+ return
50
+ end
51
+ @proj.init!
52
+ puts 'ztodo project initialized.'.green
53
+ end
54
+
55
+ def format_task task
56
+ puts task[:key].to_s+' '+ @conv.colored_priority(task[:priority]) +
57
+ ' ('+(task[:done] ? 'done'.green : 'undone'.yellow)+')'
58
+ puts task[:description].to_s.light_blue
59
+ puts ''
60
+ end
61
+
62
+ def cmd_list params=[]
63
+ load_or_die
64
+ show_all_tasks = params[0] == 'all'
65
+
66
+ tasks = @proj.tasks(show_all_tasks)
67
+ puts ''
68
+ orig = []
69
+ tasks.each do |key, task|
70
+ ind = (task[:done] ? 5 : 0) - task[:priority] + 1
71
+ orig[ind] = [] if orig[ind].nil?
72
+ orig[ind].push task
73
+ end
74
+
75
+ orig.each do |task_list|
76
+ if task_list.class == Array
77
+ task_list.each do |task|
78
+ format_task task
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ def cmd_add params=[]
85
+ load_or_die
86
+ task = @proj.create
87
+ original_key = params.join(' ')
88
+ unless original_key.empty?
89
+ task[:key] = original_key.to_sym
90
+ end
91
+ File.open(Dir.pwd+'/.ztodo-tmp', 'w+') {|f| f.write(@conv.hash_to_str task) }
92
+
93
+ exit unless system("editor .ztodo-tmp")
94
+
95
+ File.open(Dir.pwd+'/.ztodo-tmp') { |f|
96
+ task = @conv.str_to_hash f.read
97
+
98
+ begin
99
+ @proj.add task
100
+ rescue Exception, e
101
+ puts 'Task with such key already exists. Postfix added'.yellow
102
+ task[:key] += Random.rand(1000).to_s
103
+ @proj.add task
104
+ end
105
+ @proj.save!
106
+ }
107
+
108
+ File.delete Dir.pwd+'/.ztodo-tmp'
109
+ end
110
+
111
+ def cmd_modify params=[]
112
+ load_or_die
113
+ original_key = params.join(' ').to_sym
114
+ task = @proj.tasks(true)[original_key]
115
+ if task.nil?
116
+ puts 'Invalid task key'.red
117
+ exit
118
+ end
119
+ File.open(Dir.pwd+'/.ztodo-tmp', 'w+') {|f| f.write(@conv.hash_to_str task) }
120
+
121
+ exit unless system("editor .ztodo-tmp")
122
+
123
+ File.open(Dir.pwd+'/.ztodo-tmp') { |f|
124
+ task = @conv.str_to_hash f.read
125
+
126
+ begin
127
+ @proj.modify original_key, task
128
+ rescue Exception, e
129
+ puts 'Task with such key already exists. Postfix added'.yellow
130
+ task[:key] += Random.rand(1000).to_s
131
+ @proj.modify original_key, task
132
+ end
133
+ @proj.save!
134
+ }
135
+
136
+ File.delete Dir.pwd+'/.ztodo-tmp'
137
+ end
138
+
139
+ def cmd_remove params=[]
140
+ load_or_die
141
+ begin
142
+ @proj.remove params.join(' ').to_sym
143
+ rescue Exception, e
144
+ puts 'Invalid task key'.red
145
+ end
146
+ @proj.save!
147
+ end
148
+
149
+ def load_or_die
150
+ unless File.exists? Dir.pwd+'/.ztodo'
151
+ puts 'ztodo project is not initialized! Run "ztodo init"'.red
152
+ exit
153
+ end
154
+
155
+ @proj.load!
156
+ end
157
+
158
+ end
@@ -0,0 +1,106 @@
1
+ =begin
2
+ Class for converting task hashes to strings and vice versa
3
+ =end
4
+ class Ztodo::Converter
5
+
6
+ # Convert string representation of task to hash
7
+ def str_to_hash str
8
+ out = {}
9
+ data = str.split("\n")
10
+ out[:key] = parse_key_line data[0]
11
+ out[:priority] = parse_priority_line data[1]
12
+ out[:done] = parse_completeness_line data[2]
13
+ out[:description] = extract_description data.clone
14
+ out
15
+ end
16
+
17
+ # Convert hash representation of task to string
18
+ def hash_to_str hash
19
+ out = ''
20
+ out += hash[:key].to_s+" # unique identifier of task\n"
21
+ out += format_priority(hash[:priority])+" # priority: 'low', 'normal' or 'high'\n"
22
+ out += (hash[:done] ? 'done' : 'undone' )+" # task completeness: 'done' or 'undone'\n"
23
+ out += hash[:description].to_s
24
+ out
25
+ end
26
+
27
+ def colored_priority int_val
28
+ require 'colorize'
29
+ if int_val == Ztodo::Project::LOW
30
+ 'low'.green
31
+ elsif int_val == Ztodo::Project::NORMAL
32
+ 'normal'.yellow
33
+ elsif int_val == Ztodo::Project::HIGH
34
+ 'high'.red
35
+ else
36
+ raise Exception.new('Priority should be -1, 0 or 1. Given: '+int_val)
37
+ end
38
+ end
39
+
40
+ protected
41
+
42
+ # Format priority integer value as string
43
+ def format_priority int_val
44
+ if int_val == Ztodo::Project::LOW
45
+ 'low'
46
+ elsif int_val == Ztodo::Project::NORMAL
47
+ 'normal'
48
+ elsif int_val == Ztodo::Project::HIGH
49
+ 'high'
50
+ else
51
+ raise Exception.new('Priority should be -1, 0 or 1. Given: '+int_val)
52
+ end
53
+ end
54
+
55
+ # Parse line with unique identifier
56
+ def parse_key_line line
57
+ line = clear_line line
58
+ if line.nil? || line.empty?
59
+ 'task'+Random.rand(1000).to_s
60
+ else
61
+ line
62
+ end
63
+ end
64
+
65
+ # Parse line with priority data
66
+ def parse_priority_line line
67
+ return Ztodo::Project::NORMAL if line.nil? || line.empty?
68
+ line = clear_line line
69
+
70
+ if line == 'low'
71
+ Ztodo::Project::LOW
72
+ elsif line == 'high'
73
+ Ztodo::Project::HIGH
74
+ else # 'normal' and any other option
75
+ Ztodo::Project::NORMAL
76
+ end
77
+ end
78
+
79
+ # Parse line with completeness data
80
+ def parse_completeness_line line
81
+ line = clear_line line
82
+ return line == 'done'
83
+ end
84
+
85
+
86
+ # Return line without commented part
87
+ def clear_line line
88
+ return nil if line.nil?
89
+
90
+ if line.index('#').nil?
91
+ res = line
92
+ else
93
+ res = line[0,line.index('#')]
94
+ end
95
+ res.strip!
96
+ return res
97
+ end
98
+
99
+ # Return description data
100
+ def extract_description splitted_data
101
+ return '' if splitted_data.size<=3
102
+ 3.times { splitted_data.slice! 0 }
103
+ splitted_data.join "\n"
104
+ end
105
+
106
+ end
@@ -0,0 +1,73 @@
1
+ require 'yaml'
2
+
3
+ =begin
4
+ Represent Project object.
5
+ =end
6
+ class Ztodo::Project
7
+
8
+ attr_reader :loaded
9
+
10
+ LOW = -1
11
+ NORMAL = 0
12
+ HIGH = 1
13
+
14
+ def initialize
15
+ @loaded = false
16
+ @data = {}
17
+ end
18
+
19
+ # Init project file (.ztodo) in current directory
20
+ def init!
21
+ @data = {}
22
+ save!
23
+ end
24
+
25
+ # load project from file in current directory
26
+ def load!
27
+ raise Exception.new('ztodo project file is not exists') unless File.exists?(Dir.pwd+'/.ztodo')
28
+
29
+ @data = YAML::load(File.open(Dir.pwd+'/.ztodo'))
30
+ @loaded = true
31
+ end
32
+
33
+ # Save project to file in current directory
34
+ def save!
35
+ File.open(Dir.pwd + '/.ztodo', 'w+') { |f| f.write(@data.to_yaml) }
36
+ @loaded = true
37
+ end
38
+
39
+ # Return all tasks
40
+ def tasks show_all = FALSE
41
+ return @data.reject {|k,v| v[:done]} if !show_all
42
+ @data.clone
43
+ end
44
+
45
+ # Add task
46
+ # Raise en exception if key is already used
47
+ def add task
48
+ raise Exception.new('Such key already exists') if @data.include?(task[:key].to_sym)
49
+ @data[task[:key].to_sym] = task
50
+ end
51
+
52
+ # Modify task
53
+ # Raise en exception if there is no task with such key
54
+ def modify key, task
55
+ raise Exception.new('No task with such key') unless @data.include?(key.to_sym)
56
+ @data.delete key
57
+ add task
58
+ end
59
+
60
+ # Remove task
61
+ # Raise en exception if there is no task with such key
62
+ def remove key
63
+ raise Exception.new('No task with such key') unless @data.include?(key.to_sym)
64
+ @data.delete key
65
+ end
66
+
67
+ # Create task (but not add it to data)
68
+ def create
69
+ {:key=>'key', :description=>'put description here',
70
+ :done=>false, :priority=> Ztodo::Project::NORMAL}
71
+ end
72
+
73
+ end
@@ -0,0 +1,3 @@
1
+ module Ztodo
2
+ VERSION = "1.0.0"
3
+ end
data/lib/ztodo.rb ADDED
@@ -0,0 +1,10 @@
1
+ require "ztodo/version"
2
+ require "ztodo/controller"
3
+ require "ztodo/project"
4
+ require "ztodo/converter"
5
+
6
+ =begin
7
+ Module for Console Task Manager
8
+ =end
9
+ module Ztodo
10
+ end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ztodo::Converter do
4
+
5
+ before(:all) { @conv = Ztodo::Converter.new }
6
+
7
+ it 'can convert string to task hash' do
8
+ input = "test_title # unique identifier of task\n" +
9
+ "low # priority: 'low', 'normal' or 'high'\n" +
10
+ "done # task completeness: 'done' or 'undone'\n"+
11
+ "Here will be task\n" +
12
+ "Maybe in two lines"
13
+
14
+ output = {:key=>'test_title', :priority=>Ztodo::Project::LOW,
15
+ :description=>"Here will be task\nMaybe in two lines", :done=>true}
16
+
17
+ out = @conv.str_to_hash input
18
+ out.should eq(output)
19
+ end
20
+
21
+ it 'can convert task hash to string' do
22
+ input = {:key=>'my_task', :priority=>Ztodo::Project::HIGH,
23
+ :description=>"Task description\nwith two lines", :done=>true}
24
+
25
+ output = "my_task # unique identifier of task\n" +
26
+ "high # priority: 'low', 'normal' or 'high'\n" +
27
+ "done # task completeness: 'done' or 'undone'\n"+
28
+ "Task description\nwith two lines"
29
+
30
+ out = @conv.hash_to_str input
31
+ out.should eq(output)
32
+ end
33
+
34
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ztodo::Project do
4
+
5
+ it 'has methods: init, save and load' do
6
+ methods = [:init!, :save!, :load!]
7
+ methods.each { |method_name| Ztodo::Project.methods.include?(method_name) }
8
+ end
9
+
10
+ it 'can add tasks' do
11
+ p = Ztodo::Project.new
12
+ task = {:key=>'task_A', :priority=>Ztodo::Project::NORMAL,
13
+ :description=>"description for A task", :done=>false}
14
+ p.add task
15
+ tasks = p.tasks
16
+ tasks[:task_A].should eq(task)
17
+ end
18
+
19
+ it 'can modify tasks' do
20
+ p = Ztodo::Project.new
21
+ task = {:key=>'task_A', :priority=>Ztodo::Project::NORMAL,
22
+ :description=>"description for A task", :done=>false}
23
+ task2 = {:key=>'task_B', :priority=>Ztodo::Project::HIGH,
24
+ :description=>"description for B task", :done=>true}
25
+ p.add task
26
+ p.modify :task_A, task2
27
+
28
+ tasks = p.tasks true
29
+ tasks[:task_A].should eq(nil)
30
+ tasks[:task_B].should eq(task2)
31
+ end
32
+
33
+ it 'can delete tasks' do
34
+ p = Ztodo::Project.new
35
+ task = {:key=>'task_A', :priority=>Ztodo::Project::NORMAL,
36
+ :description=>"description for A task", :done=>false}
37
+ p.add task
38
+ p.tasks(true)[:task_A].should eq(task)
39
+ p.remove :task_A
40
+ p.tasks(true)[:task_A].should eq(nil)
41
+ end
42
+
43
+ it 'can list uncompleted tasks' do
44
+ p = Ztodo::Project.new
45
+
46
+ task = {:key=>'task_A', :priority=>Ztodo::Project::NORMAL,
47
+ :description=>"description for A task", :done=>false}
48
+ task2 = {:key=>'task_B', :priority=>Ztodo::Project::NORMAL,
49
+ :description=>"description for B task", :done=>true}
50
+ p.add task
51
+ p.add task2
52
+
53
+ tasks = p.tasks
54
+ tasks[:task_B].should eq(nil)
55
+ tasks[:task_A].should eq(task)
56
+
57
+ tasks = p.tasks true
58
+ tasks.size.should eq(2)
59
+ end
60
+
61
+ end
@@ -0,0 +1,27 @@
1
+ require 'ztodo'
2
+
3
+ RSpec.configure do |config|
4
+ config.treat_symbols_as_metadata_keys_with_true_values = true
5
+ config.run_all_when_everything_filtered = true
6
+ config.filter_run :focus
7
+
8
+ config.order = 'random'
9
+
10
+ config.before(:all) do
11
+ if Dir.exists?(Dir.pwd + '/tmp')
12
+ @old_dir = Dir.pwd
13
+ Dir.chdir (Dir.pwd + '/tmp')
14
+ end
15
+ end
16
+
17
+ config.after(:all) do
18
+ unless Dir.pwd == @old_dir
19
+ Dir.chdir @old_dir
20
+ end
21
+ end
22
+
23
+ config.after(:each) do
24
+ File.delete(Dir.pwd+'/.todo') if File.exists?(Dir.pwd+'/.todo')
25
+ end
26
+
27
+ end
data/tmp/.gitkeep ADDED
File without changes
data/ztodo.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ztodo/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "ztodo"
8
+ gem.version = Ztodo::VERSION
9
+ gem.authors = ["Andreas Sotnik"]
10
+ gem.email = ["andreas.sotnik@gmail.com"]
11
+ gem.description = 'Simple Console Task Manager. Check http://zenturio.me/pages/ztodo for further information.'
12
+ gem.summary = 'Simple Console Task Manager'
13
+ gem.homepage = "http://zenturio.me/pages/ztodo"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency('colorize')
21
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ztodo
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Andreas Sotnik
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-18 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: colorize
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: Simple Console Task Manager. Check http://zenturio.me/pages/ztodo for
31
+ further information.
32
+ email:
33
+ - andreas.sotnik@gmail.com
34
+ executables:
35
+ - ztodo.rb
36
+ extensions: []
37
+ extra_rdoc_files: []
38
+ files:
39
+ - .gitignore
40
+ - .rspec
41
+ - Gemfile
42
+ - Gemfile.lock
43
+ - LICENSE.txt
44
+ - README.md
45
+ - Rakefile
46
+ - bin/ztodo.rb
47
+ - lib/ztodo.rb
48
+ - lib/ztodo/controller.rb
49
+ - lib/ztodo/converter.rb
50
+ - lib/ztodo/project.rb
51
+ - lib/ztodo/version.rb
52
+ - spec/converter_spec.rb
53
+ - spec/project_spec.rb
54
+ - spec/spec_helper.rb
55
+ - tmp/.gitkeep
56
+ - ztodo.gemspec
57
+ homepage: http://zenturio.me/pages/ztodo
58
+ licenses: []
59
+ post_install_message:
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ! '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ requirements: []
76
+ rubyforge_project:
77
+ rubygems_version: 1.8.24
78
+ signing_key:
79
+ specification_version: 3
80
+ summary: Simple Console Task Manager
81
+ test_files:
82
+ - spec/converter_spec.rb
83
+ - spec/project_spec.rb
84
+ - spec/spec_helper.rb