momm 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +118 -0
  6. data/Rakefile +16 -0
  7. data/bin/momm +72 -0
  8. data/lib/momm/calculator.rb +129 -0
  9. data/lib/momm/feeds/ecb.rb +67 -0
  10. data/lib/momm/memcached.rb +22 -0
  11. data/lib/momm/redis_store.rb +24 -0
  12. data/lib/momm/storage.rb +68 -0
  13. data/lib/momm/version.rb +3 -0
  14. data/lib/momm/web.rb +28 -0
  15. data/lib/momm.rb +92 -0
  16. data/momm.gemspec +30 -0
  17. data/myapp/.gitignore +16 -0
  18. data/myapp/Gemfile +10 -0
  19. data/myapp/README.rdoc +28 -0
  20. data/myapp/Rakefile +6 -0
  21. data/myapp/app/assets/images/.keep +0 -0
  22. data/myapp/app/assets/javascripts/application.js +15 -0
  23. data/myapp/app/assets/stylesheets/application.css +13 -0
  24. data/myapp/app/controllers/application_controller.rb +5 -0
  25. data/myapp/app/controllers/concerns/.keep +0 -0
  26. data/myapp/app/helpers/application_helper.rb +2 -0
  27. data/myapp/app/mailers/.keep +0 -0
  28. data/myapp/app/models/.keep +0 -0
  29. data/myapp/app/models/concerns/.keep +0 -0
  30. data/myapp/app/views/layouts/application.html.erb +14 -0
  31. data/myapp/bin/bundle +3 -0
  32. data/myapp/bin/rails +4 -0
  33. data/myapp/bin/rake +4 -0
  34. data/myapp/config/application.rb +28 -0
  35. data/myapp/config/boot.rb +4 -0
  36. data/myapp/config/environment.rb +5 -0
  37. data/myapp/config/environments/development.rb +26 -0
  38. data/myapp/config/environments/production.rb +80 -0
  39. data/myapp/config/environments/test.rb +36 -0
  40. data/myapp/config/initializers/backtrace_silencers.rb +7 -0
  41. data/myapp/config/initializers/filter_parameter_logging.rb +4 -0
  42. data/myapp/config/initializers/inflections.rb +16 -0
  43. data/myapp/config/initializers/mime_types.rb +5 -0
  44. data/myapp/config/initializers/momm.rb +2 -0
  45. data/myapp/config/initializers/secret_token.rb +12 -0
  46. data/myapp/config/initializers/session_store.rb +3 -0
  47. data/myapp/config/initializers/wrap_parameters.rb +9 -0
  48. data/myapp/config/locales/en.yml +23 -0
  49. data/myapp/config/routes.rb +5 -0
  50. data/myapp/config.ru +4 -0
  51. data/myapp/db/seeds.rb +7 -0
  52. data/myapp/lib/assets/.keep +0 -0
  53. data/myapp/lib/tasks/.keep +0 -0
  54. data/myapp/log/.keep +0 -0
  55. data/myapp/public/404.html +58 -0
  56. data/myapp/public/422.html +58 -0
  57. data/myapp/public/500.html +57 -0
  58. data/myapp/public/favicon.ico +0 -0
  59. data/myapp/public/robots.txt +5 -0
  60. data/myapp/vendor/assets/javascripts/.keep +0 -0
  61. data/myapp/vendor/assets/stylesheets/.keep +0 -0
  62. data/spec/momm/calculator_spec.rb +79 -0
  63. data/spec/momm/feeds/ecb_spec.rb +48 -0
  64. data/spec/momm/memcached_spec.rb +32 -0
  65. data/spec/momm/momm_spec.rb +21 -0
  66. data/spec/momm/redis_store_spec.rb +35 -0
  67. data/spec/momm/storage_spec.rb +37 -0
  68. data/spec/momm/version_spec.rb +7 -0
  69. data/spec/momm/web_spec.rb +31 -0
  70. data/spec/spec_helper.rb +11 -0
  71. metadata +235 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8bfaf35e8c7c6d5a3df2b10c78e0d840de8903b9
4
+ data.tar.gz: 46e99175387cb4ab1823d858a0b4e60afbdfdefb
5
+ SHA512:
6
+ metadata.gz: 31d92303e463cd6b831855472f9e8595a75972ef6c579e6288e1487cf1f7ec6973188c20be089419ce8ad71296c5aaf067dcba9a3a1d1d39c3dd699b9b931fba
7
+ data.tar.gz: e02cb0d88441d39df19ea6a86780d30b24bd1ebf5f932b52df83214ec0ab10f53313ccb34d77c61e056571459a6e9968958d48169dd90e0542fc023c26ee88d6
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in momm.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Jingkai He
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,118 @@
1
+ # Momm
2
+
3
+ Money on My Mind - An awesome gem for currency exchange.
4
+
5
+ ```
6
+ __ __ __ __
7
+ | \/ | | \/ |
8
+ | \ / | ___ _ __ ___ _ _ ___ _ __ | \ / |_ _
9
+ | |\/| |/ _ \| '_ \ / _ \ | | | / _ \| '_ \ | |\/| | | | |
10
+ | | | | (_) | | | | __/ |_| | | (_) | | | | | | | | |_| |
11
+ |_| |_|\___/|_| |_|\___|\__, | \___/|_| |_| |_| |_|\__, |
12
+ __/ | __/ |
13
+ |___/ |___/
14
+ __ __ _ _
15
+ | \/ (_) | |
16
+ | \ / |_ _ __ __| |
17
+ | |\/| | | '_ \ / _` |
18
+ | | | | | | | | (_| |
19
+ |_| |_|_|_| |_|\__,_|
20
+
21
+
22
+ Keep calm bro. We'll calculate rate for you.
23
+
24
+ ```
25
+
26
+ ## Requirement
27
+
28
+ Ensure that [Memcached](http://memcached.org/) or [Redis](http://redis.io/) is installed on your local machine or server or provided by some other cloud service providers. Those are for exchange rate storage, which ensure your fast queries.
29
+
30
+ Local storage is not provided, because it might not safe for work on Cloud Platforms such as Heroku.
31
+
32
+ ## Installation
33
+
34
+ Add this line to your application's Gemfile:
35
+
36
+ gem 'momm'
37
+
38
+ And then execute:
39
+
40
+ $ bundle
41
+
42
+ Or install it yourself as:
43
+
44
+ $ gem install momm
45
+
46
+ ## Usage
47
+
48
+ ### Off Rails
49
+
50
+ #### Command Line Tool
51
+
52
+ The storage engine by default is Redis, ensure the socket is opened.
53
+
54
+ ```
55
+ $ momm rate GBP CNY # Exchange rate by default is today.
56
+ $ momm exchange 100 GBP USD 2014-3-1 # Exchange rate at 2013-3-1
57
+ ```
58
+
59
+ If you want to change the storage strategies, edit it in your ~/.mom/config.yml file.
60
+
61
+ #### Ruby
62
+
63
+ ``` ruby
64
+ require 'momm'
65
+
66
+ Momm.exchange_rate 'GBP', 'USD' # By default Today
67
+ Momm.exchange_rate 'GBP', 'USD', date: Date.today
68
+ Momm.exchange_rate_from_gbp_to_usd, date: "2014-3-4"
69
+
70
+ Momm.exchange 100, 'GBP', 'USD'
71
+ Momm.exchange 100, 'GBP', 'USD', date: Date.today
72
+ Momm.exchange_from_gbp_to_usd 100
73
+ Momm.exchange_from_gbp_to_usd 100, date: "2014-3-4"
74
+
75
+ ```
76
+
77
+ #### Configuration
78
+
79
+ ``` ruby
80
+
81
+ Momm.store :redis_store # Use redis as the default storage
82
+
83
+ Momm.fed :ECB # Use ECB as the default currency exchange feeds
84
+
85
+ ```
86
+
87
+ ### Momm on Rails
88
+
89
+ Web service is provided by Momm on Rails, however you need to install sinatra simply by adding ```gem 'sinatra'``` into your Gemfile. Then require ```momm/web``` module and edit your routes like:
90
+
91
+ ``` ruby
92
+ # routes.rb
93
+
94
+ require 'momm/web'
95
+
96
+ Myapp::Application.routes.draw do
97
+ mount Momm::Web => '/momm'
98
+ end
99
+ ```
100
+
101
+ The default Storage is Memcached, if you want to switch to Redis, you can create an intialzier like:
102
+
103
+ ``` ruby
104
+
105
+ # momm_initialzer.rb
106
+ Momm.store :redis_store, host: "127.0.0.1"
107
+ Momm.source :ECB
108
+ ```
109
+
110
+ ### @TODO: A small widget
111
+
112
+ ## Contributing
113
+
114
+ 1. Fork it ( https://github.com/jaxi/momm/fork )
115
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
116
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
117
+ 4. Push to the branch (`git push origin my-new-feature`)
118
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ desc "Run an IRB session with Momm preloaded"
5
+ task :console do
6
+ exec "irb -I lib -r momm"
7
+ end
8
+
9
+ desc "Run the test suite"
10
+ RSpec::Core::RakeTask.new(:rspec) do |t|
11
+ t.pattern = FileList['spec/**/*_spec.rb']
12
+ t.rspec_opts = %w|--color|
13
+ end
14
+
15
+ task default: :spec
16
+
data/bin/momm ADDED
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH << File.dirname(__FILE__) + "/../lib" if $0 == __FILE__
3
+
4
+ require 'optparse'
5
+ require 'momm'
6
+
7
+ OPERATIONS = %w{rate exchange}
8
+
9
+ option_parser = OptionParser.new do |opts|
10
+ opts.banner = Momm::BANNER
11
+
12
+ opts.separator <<-EOS
13
+
14
+ Supported commands:
15
+
16
+ rate [country_code] [country_code] [YYYY-MM-DD] Display rate!
17
+ exchange [money] [country_code] [country_code] [YYYY-MM-DD] Exchange for you!
18
+
19
+ Examples:
20
+ mmom exchange 20 GBP CNY Convert the between currencies
21
+ mmom rate GBP CNY Simply display the rate
22
+
23
+ Support Code:
24
+
25
+ #{Momm.currencies.join(" ")}
26
+
27
+ To be honest I don't know most of them...
28
+
29
+ EOS
30
+ end
31
+
32
+ option_parser.parse!
33
+
34
+
35
+ op = ARGV.shift
36
+ if OPERATIONS.include?(op)
37
+ begin
38
+ case op
39
+ when "rate"
40
+ from = ARGV[0].to_sym
41
+ to = ARGV[1].to_sym
42
+ date = if ARGV[2]
43
+ Date.parse(ARGV[2])
44
+ else
45
+ Date.today
46
+ end
47
+
48
+ puts Momm.exchange_rate from, to, date: date
49
+ when "exchange"
50
+ money = ARGV[0].to_f
51
+ from = ARGV[1].to_sym
52
+ to = ARGV[2].to_sym
53
+ date = if ARGV[3]
54
+ Date.parse(ARGV[3])
55
+ else
56
+ Date.today
57
+ end
58
+
59
+ puts Momm.exchange money, from, to, date: date
60
+ end
61
+
62
+ rescue ArgumentError => ex
63
+ puts ex.message
64
+
65
+ rescue Exception => e
66
+ puts "Mmmmm, I didn't expect this:"
67
+ puts e.message
68
+ puts e.backtrace.join("\n")
69
+ end
70
+ else
71
+ puts option_parser.help
72
+ end
@@ -0,0 +1,129 @@
1
+ module Momm
2
+ class Calculator
3
+ extend ::Forwardable
4
+ # Initialise Storage Object
5
+ #
6
+ # == Parameters:
7
+ # client::
8
+ # A storage, such as memcached, redis
9
+ # == Returns
10
+ # self
11
+ #
12
+ def initialize(storage = Memcached.new, feed = Feeds::ECB.instance)
13
+ @storage = storage
14
+ @feed = feed
15
+ end
16
+
17
+ attr_reader :storage, :feed
18
+
19
+ # delegate the client and update method from storage
20
+ delegate [:client, :update, :set_rate] => :storage
21
+
22
+ # delegate the currencies method from feed
23
+ delegate :currencies => :feed
24
+
25
+ # delegate the get rate method with a different naming
26
+ def_delegator :storage, :get_rate, :get_rate_origin
27
+
28
+ # Exchange Rate
29
+ #
30
+ # == Parameters
31
+ # from::
32
+ # ruby symbol, such as :USD, :GBP
33
+ # to::
34
+ # same as above
35
+ # options::
36
+ # 0ption parameters, contain today by default
37
+ #
38
+ # == Returns
39
+ # the exchange rate
40
+ #
41
+ def exchange_rate(from, to, options = {})
42
+ date = options[:date] || Date.today
43
+ date = Date.parse(date) if date.is_a? String
44
+
45
+ max_count = 10
46
+ date_counter = date
47
+
48
+ # @TODO Refactoring.
49
+ # It seems that currency does not have feeds at weekends, so
50
+ # we simply find the closest day which has currency feeds.
51
+ while max_count > 0
52
+ to_rate = get_rate(to, date_counter)
53
+ from_rate = get_rate(from, date_counter)
54
+
55
+ date_counter -=1
56
+ max_count -= 1
57
+ next if to_rate == 0 || from_rate == 0
58
+
59
+ set_rate(to, to_rate, date)
60
+ set_rate(from, from_rate, date)
61
+
62
+ return (to_rate / from_rate).round(2)
63
+ end
64
+
65
+ 0.0 / 0
66
+ end
67
+
68
+ # Exchange Money from one currency to another
69
+ #
70
+ # == Parameters
71
+ # money::
72
+ # money you have
73
+ # from::
74
+ # ruby symbol, such as :USD, :GBP
75
+ # to::
76
+ # same as above
77
+ # options::
78
+ # option parameters, contain today by default
79
+ #
80
+ # == Returns
81
+ # money exchanged
82
+ #
83
+ def exchange(money, from, to, options= {})
84
+ options[:date] ||= Date.today
85
+ (money * exchange_rate(from, to, options)).round(2)
86
+ end
87
+
88
+ # Delegate the get_rate method, if the target is missing
89
+ # Fetching all data from remote
90
+ #
91
+ # == Parameters
92
+ #
93
+ # date::
94
+ # Default is Date.today
95
+ # currency::
96
+ # Currency passed in
97
+ # == Returns
98
+ # the currency rate
99
+ #
100
+ def get_rate(from, date = Date.today)
101
+ res = get_rate_origin(from, date)
102
+ return res if res != 0 && res
103
+
104
+ update(feed.currency_rates)
105
+ get_rate_origin(from, date)
106
+ end
107
+
108
+ # @TODO: Refactoring
109
+ def method_missing(meth, *args, &block)
110
+ meth = meth.to_s
111
+ case
112
+ when meth.match(/^exchange_rate_from_(\w+)_to_(\w+)/)
113
+ exchange_rate($1.upcase.to_sym, $2.upcase.to_sym, *args, &block)
114
+ when meth.match(/^exchange_from_(\w+)_to_(\w+)/)
115
+ money, *res = args
116
+ exchange(money, $1.upcase.to_sym, $2.upcase.to_sym, *res, &block)
117
+ else
118
+ super
119
+ end
120
+ end
121
+
122
+ def respond_to?(meth, include_private = false)
123
+ meth = meth.to_s
124
+ meth.match(/^exchange_rate_from_(\w+)_to_(\w+)/) ||
125
+ meth.match(/^exchange_from_(\w+)_to_(\w+)/) ||
126
+ super
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,67 @@
1
+ require 'httparty'
2
+ require 'open-uri'
3
+
4
+ module Momm
5
+ module Feeds
6
+ class ECB
7
+
8
+ FETCHING_URL = "http://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml".freeze
9
+
10
+ #Hard coded for good
11
+ CURRENCIES = %w{USD JPY BGN CZK DKK GBP HUF LTL PLN RON SEK CHF
12
+ NOK HRK RUB TRY AUD BRL CAD CNY HKD IDR ILS INR KRW
13
+ MXN MYR NZD PHP SGD THB ZAR}.freeze
14
+
15
+ class << self
16
+
17
+ # Eager loading the instance of ECB
18
+ #
19
+ # == Returns
20
+ # self
21
+ #
22
+ def instance
23
+ @instance ||= self.send :new
24
+ end
25
+ end
26
+
27
+ # should be a singleton class
28
+ private_class_method :new
29
+
30
+ # Parse the XML data by Nokogiri
31
+ # == Returns
32
+ # Nokogiri Object
33
+ #
34
+ def parsed_content
35
+ # @TODO Refactoring Bad patterns
36
+ HTTParty.get(fetching_url, format: :xml)['Envelope']['Cube']['Cube']
37
+ end
38
+
39
+ # convert the nokogiri parsed data to array
40
+ #
41
+ # == Returns
42
+ # looks like [{date: Date.now, currency: :CNY, rate: 1.23} ...]
43
+ #
44
+ def currency_rates
45
+ parsed_content.map do |content|
46
+ date = Date.parse(content["time"])
47
+ cubes = content["Cube"]
48
+ cubes.map do |cube|
49
+ {
50
+ date: date,
51
+ currency: cube["currency"].to_sym,
52
+ rate: cube["rate"].to_f
53
+ }
54
+ end
55
+ end.flatten
56
+ end
57
+
58
+ def fetching_url
59
+ FETCHING_URL
60
+ end
61
+
62
+ def currencies
63
+ CURRENCIES
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,22 @@
1
+ require 'dalli'
2
+
3
+ module Momm
4
+ class Memcached < Storage
5
+
6
+ DEFAULT_OPTIONS = {
7
+ connection: "localhost:11211", namespace: "momm", compress: true
8
+ }.freeze
9
+
10
+ attr_reader :connection, :options
11
+
12
+ def initialize(options={})
13
+ _options = DEFAULT_OPTIONS.dup.merge options
14
+ @connection = options.delete(:connection)
15
+ @options = options
16
+ end
17
+
18
+ def client
19
+ @client ||= Dalli::Client.new connection, options
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ require 'redis'
2
+
3
+ module Momm
4
+ class RedisStore < Storage
5
+ DEFAULT_OPTIONS = { host: "localhost", port: 6379, namespace: "momm"}
6
+
7
+ attr_accessor :options
8
+
9
+ def initialize(options = {})
10
+ @options = DEFAULT_OPTIONS.dup.merge options
11
+ end
12
+
13
+ def client
14
+ @client ||= begin
15
+ ns = options.delete(:namespace)
16
+
17
+ require 'redis/namespace'
18
+ native_client = Redis.new options
19
+
20
+ Redis::Namespace.new(ns, :redis => native_client)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,68 @@
1
+ module Momm
2
+ class Storage
3
+ # The presenter model to rule the behavior of storage.
4
+
5
+ # Lazy load Client
6
+ #
7
+ # == Returns
8
+ # Client
9
+ #
10
+ def client
11
+ raise NotImplementedError
12
+ end
13
+
14
+ # Insert the currency rate to client
15
+ #
16
+ # == Parameters
17
+ # currency::
18
+ # Currency passed in, should be a symbol of iso code,
19
+ # such as :CNY, :USD, or :GBP
20
+ # rate::
21
+ # Fixnum represent for the currency rate
22
+ # from euro to certain currency
23
+ # date::
24
+ # Ruby date type, the date of currency rate, today by default
25
+ #
26
+ # == Returns
27
+ # nil
28
+ #
29
+ def set_rate(currency, rate, date = Date.today)
30
+ date = Date.parse(date) if date.is_a? String
31
+ client.set "#{date}#{currency}", rate
32
+ end
33
+
34
+ # Fetch the currency rate from client
35
+ #
36
+ # == Parameters
37
+ # currency::
38
+ # Currency passed in
39
+ # date::
40
+ # Ruby date type, the date of currency rate, today by default
41
+ #
42
+ # == Returns
43
+ # the currency rate
44
+ #
45
+ def get_rate(currency, date = Date.today)
46
+ date = Date.parse(date) if date.is_a? String
47
+ client.get("#{date}#{currency}").to_f
48
+ end
49
+
50
+ # update the data to storage
51
+ #
52
+ # == parameters
53
+ # data::
54
+ # An array looks like [{date: Date.now, currency: :CNY, rate: 1.23} ...]
55
+ #
56
+ # == Returns
57
+ # nil
58
+ #
59
+ def update(data)
60
+ data.each do |d|
61
+ set_rate d[:currency], d[:rate], d[:date]
62
+ end
63
+ nil
64
+ end
65
+
66
+ NotImplementedError = Class.new(StandardError)
67
+ end
68
+ end
@@ -0,0 +1,3 @@
1
+ module Momm
2
+ VERSION = "0.0.1"
3
+ end
data/lib/momm/web.rb ADDED
@@ -0,0 +1,28 @@
1
+ require 'sinatra/base'
2
+ require 'momm'
3
+
4
+ module Momm
5
+ class Web < ::Sinatra::Base
6
+ get '/query' do
7
+ content_type :json
8
+
9
+ money = params[:money].to_f
10
+ from = params[:from].to_sym
11
+ to = params[:to].to_sym
12
+ date = params[:date] || Date.today
13
+
14
+
15
+ if money && from && to && date
16
+ Momm.exchange(money, from, to, date: date).to_json
17
+ else
18
+ "N/A"
19
+ end
20
+
21
+ end
22
+
23
+ get '/currencies' do
24
+ content_type :json
25
+ Momm.currencies.to_json
26
+ end
27
+ end
28
+ end