dolarblue 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -2
- data/.rspec +1 -1
- data/.ruby-gemset +2 -0
- data/.ruby-version +1 -1
- data/.travis.yml +9 -4
- data/CHANGELOG.md +9 -4
- data/Gemfile +20 -0
- data/Gemfile.lock +110 -0
- data/README.md +12 -11
- data/Rakefile +3 -2
- data/config/xpaths.yml +17 -0
- data/dolarblue.gemspec +20 -16
- data/lib/dolarblue.rb +9 -4
- data/lib/dolarblue/blue.rb +7 -0
- data/lib/dolarblue/card.rb +20 -0
- data/lib/dolarblue/configuration.rb +17 -48
- data/lib/dolarblue/inflector.rb +23 -0
- data/lib/dolarblue/instance_methods.rb +88 -80
- data/lib/dolarblue/official.rb +7 -0
- data/lib/dolarblue/version.rb +1 -1
- data/lib/dolarblue/xchange.rb +86 -0
- data/spec/blue_spec.rb +5 -0
- data/spec/cassettes/.keep +0 -0
- data/spec/dolarblue_spec.rb +17 -87
- data/spec/spec_helper.rb +21 -11
- metadata +75 -58
- data/lib/dolarblue/class_methods.rb +0 -11
- data/lib/dolarblue/exchange.rb +0 -108
- data/spec/exchange_spec.rb +0 -92
@@ -0,0 +1,23 @@
|
|
1
|
+
class Dolarblue
|
2
|
+
module Inflector
|
3
|
+
extend self
|
4
|
+
|
5
|
+
# Removes the module part from the expression in the string.
|
6
|
+
#
|
7
|
+
# @param path [String] the module expression stringified
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# demodulize('ActiveRecord::CoreExtensions::String::Inflections') # => "Inflections"
|
11
|
+
# demodulize('Inflections') # => "Inflections"
|
12
|
+
#
|
13
|
+
# @return [String] with the module part removed and the stringified class name only
|
14
|
+
def demodulize(path)
|
15
|
+
path = path.to_s
|
16
|
+
if i = path.rindex('::')
|
17
|
+
path[(i+2)..-1]
|
18
|
+
else
|
19
|
+
path
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -1,94 +1,102 @@
|
|
1
|
+
require 'dolarblue/configuration'
|
2
|
+
require 'dolarblue/blue'
|
3
|
+
require 'dolarblue/official'
|
4
|
+
require 'dolarblue/card'
|
5
|
+
|
6
|
+
require 'nokogiri' # gem 'nokogiri'
|
7
|
+
require 'open-uri' # stdlib
|
8
|
+
|
1
9
|
class Dolarblue
|
2
|
-
module InstanceMethods
|
3
|
-
|
4
|
-
# Create a new Dolarblue instance to work later on
|
5
|
-
#
|
6
|
-
# @param [Configuration] config the configuration instance
|
7
|
-
#
|
8
|
-
# @return [Dolarblue] new instance
|
9
|
-
def initialize(config = Configuration.instance)
|
10
|
-
fail ArgumentError, "Expected a Dolarblue::Configuration instance as argument" unless config.is_a?(Configuration)
|
11
|
-
@card_fee = config.card_fee
|
12
|
-
@blue = Dolarblue::Exchange.new('Blue', config.blue_screen_name, config.blue_regexp, config.buy_sell_blue_factor)
|
13
|
-
@official = Dolarblue::Exchange.new('Official', config.official_screen_name, config.official_regexp, config.buy_sell_official_factor)
|
14
|
-
self
|
15
|
-
end
|
16
10
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
11
|
+
# Create a new Dolarblue instance to work later on
|
12
|
+
#
|
13
|
+
# @param config [Configuration] the configuration instance
|
14
|
+
#
|
15
|
+
# @return [Dolarblue] new instance
|
16
|
+
def initialize(config = Configuration.instance)
|
17
|
+
@config = config.defaults
|
18
|
+
@blue = Blue.new
|
19
|
+
@official = Official.new
|
20
|
+
@card = Card.new
|
21
|
+
@output = nil
|
22
|
+
self
|
23
|
+
end
|
23
24
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
print "Done.\n\n"
|
32
|
-
self
|
33
|
-
end
|
25
|
+
# Connect to the source and retrieve dollar exchange values
|
26
|
+
#
|
27
|
+
# @return (see #initialize)
|
28
|
+
def update!
|
29
|
+
@output = ''
|
30
|
+
base_url = @config.base_url
|
31
|
+
fail ArgumentError, "Need base_url configuration to know where to web-scrape from. Current value: #{base_url}" if base_url.empty?
|
34
32
|
|
35
|
-
|
36
|
-
|
37
|
-
# @return [Float] percentile value between 0..1
|
38
|
-
def gap_official
|
39
|
-
fail "Need blue and official values to be setup before calculating the gap" unless @blue.sell_value && @blue.sell_value > 0 && @official.sell_value && @official.sell_value > 0
|
40
|
-
(@blue.sell_value / @official.sell_value - 1)
|
41
|
-
end
|
33
|
+
log "Obtaining latest AR$ vs US$ exchange values..."
|
34
|
+
html_file = open(base_url)
|
42
35
|
|
43
|
-
|
44
|
-
|
45
|
-
# @return [Float] percentile value between 0..100
|
46
|
-
def gap_official_percent
|
47
|
-
(gap_official * 100).round(0)
|
48
|
-
end
|
36
|
+
log "Parsing values..."
|
37
|
+
parse_values Nokogiri::HTML(html_file)
|
49
38
|
|
50
|
-
|
51
|
-
|
52
|
-
|
39
|
+
log "\nDone: #{Time.now.localtime}\n"
|
40
|
+
self
|
41
|
+
end
|
53
42
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
43
|
+
# Returns the gap percentile (e.g. 60) between the real (blue) dollar value versus the official
|
44
|
+
#
|
45
|
+
# @return [Float] percentile value between 0..100
|
46
|
+
def gap_official_percent
|
47
|
+
gap_official = @blue.sell / @official.sell - 1
|
48
|
+
(gap_official * 100).round(0)
|
49
|
+
end
|
61
50
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
51
|
+
# Returns the gap percentile between the real (blue) dollar value versus the official
|
52
|
+
#
|
53
|
+
# @return (see #gap_official_percent)
|
54
|
+
def gap_card_percent
|
55
|
+
gap_card = @blue.sell / @card.sell - 1
|
56
|
+
(gap_card * 100).round(0)
|
57
|
+
end
|
68
58
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
59
|
+
# Output string to be used by the binary `dolarblue`
|
60
|
+
#
|
61
|
+
# @return [String] the output with dollar exchange information
|
62
|
+
def output
|
63
|
+
<<-OUTPUT
|
64
|
+
#{@output}
|
65
|
+
#{@official.output}
|
66
|
+
#{@card.output}
|
67
|
+
#{@blue.output}
|
68
|
+
|
69
|
+
- Gap card.......blue: #{gap_card_percent}%
|
70
|
+
- Gap official...blue: #{gap_official_percent}%
|
75
71
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
#
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
72
|
+
Information source:
|
73
|
+
#{@config.base_url}
|
74
|
+
OUTPUT
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Parse dollar related values using the XPath configured
|
80
|
+
#
|
81
|
+
# @param doc [Nokogiri::HTML] the html document to extract values from
|
82
|
+
#
|
83
|
+
# @return [true, false] boolean If parsed successfully or not
|
84
|
+
def parse_values(doc)
|
85
|
+
[@blue, @official, @card].each do |type|
|
86
|
+
type.extract_values(doc)
|
91
87
|
end
|
88
|
+
end
|
92
89
|
|
90
|
+
# Poor man's logger to keep user updated with http get activity
|
91
|
+
# while allowing to buffer the string while in RSpec test mode
|
92
|
+
#
|
93
|
+
# @param msg [String] the message to print or buffer
|
94
|
+
def log(msg)
|
95
|
+
if defined?(RSpec)
|
96
|
+
@output << msg
|
97
|
+
else
|
98
|
+
print msg
|
99
|
+
end
|
93
100
|
end
|
101
|
+
|
94
102
|
end
|
data/lib/dolarblue/version.rb
CHANGED
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'dolarblue/configuration'
|
2
|
+
require 'dolarblue/inflector'
|
3
|
+
|
4
|
+
class Dolarblue
|
5
|
+
|
6
|
+
# Base class for Blue, Official and Card used to hold sell/buy values functionality
|
7
|
+
#
|
8
|
+
# @abstract
|
9
|
+
class XChange
|
10
|
+
attr_reader :buy, :sell
|
11
|
+
|
12
|
+
# Create a new Blue / Official / Card instance to work later on
|
13
|
+
#
|
14
|
+
# @param config [Configuration] the configuration instance
|
15
|
+
#
|
16
|
+
# @return [self] new instance
|
17
|
+
def initialize(config = Configuration.instance)
|
18
|
+
@config = config.defaults
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
# Return the demodulized class name
|
23
|
+
#
|
24
|
+
# @return [String] demodulized class name string
|
25
|
+
def cname
|
26
|
+
Inflector.demodulize(self.class.name)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Return downcased demodulized class name
|
30
|
+
#
|
31
|
+
# @return [String] downcased demodulized class name string
|
32
|
+
def name
|
33
|
+
cname.downcase
|
34
|
+
end
|
35
|
+
|
36
|
+
# Performs buy and sell values extraction from a Nokogiri::HTML Document
|
37
|
+
#
|
38
|
+
# @param [Nokogiri::HTML] doc the html document to extract values from
|
39
|
+
def extract_values(doc)
|
40
|
+
@buy = extract_val(doc, 'buy')
|
41
|
+
@sell = extract_val(doc, 'sell')
|
42
|
+
end
|
43
|
+
|
44
|
+
# Return a formatted string suitable for user output with current buy value
|
45
|
+
#
|
46
|
+
# @return [String] formatted output buy exchange value
|
47
|
+
def buy_output
|
48
|
+
'%.2f' % buy
|
49
|
+
end
|
50
|
+
|
51
|
+
# Return a formatted string suitable for user output with current sell value
|
52
|
+
#
|
53
|
+
# @return [String] formatted output sell exchange value
|
54
|
+
def sell_output
|
55
|
+
'%.2f' % sell
|
56
|
+
end
|
57
|
+
|
58
|
+
# Return a formatted string suitable for user output with current buy and sell values
|
59
|
+
#
|
60
|
+
# @return [String] formatted output with buy and sell exchange values
|
61
|
+
def output
|
62
|
+
t = cname.ljust(10, '.')
|
63
|
+
b = buy_output.rjust(5)
|
64
|
+
s = sell_output.rjust(5)
|
65
|
+
%Q{- Dollar #{t}..: #{b} / #{s}}
|
66
|
+
end
|
67
|
+
|
68
|
+
protected
|
69
|
+
|
70
|
+
# Extract individual buy/sell values from a Nokogiri::HTML Document
|
71
|
+
#
|
72
|
+
# @param doc [Nokogiri::HTML] the html document to extract values from
|
73
|
+
# @param type [String] the dollar type, can be 'blue', 'official', 'card'
|
74
|
+
#
|
75
|
+
# @raise [RuntimeError] if unable to proper buy/sell value for current dollar type
|
76
|
+
#
|
77
|
+
# @private
|
78
|
+
def extract_val(doc, type)
|
79
|
+
xpath = @config[name][type].xpath
|
80
|
+
value = doc.xpath(xpath).text.gsub(',', '.')
|
81
|
+
fail RuntimeError, "Failed to capture #{name} #{type} value" if value.empty?
|
82
|
+
value.to_f.round(2)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
data/spec/blue_spec.rb
ADDED
File without changes
|
data/spec/dolarblue_spec.rb
CHANGED
@@ -1,92 +1,22 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
before do
|
22
|
-
Twitter::Client.stub_chain(:new, :last_tweet).with('DolarBlue').and_return(blue_tweet)
|
23
|
-
Twitter::Client.stub_chain(:new, :last_tweet).with('cotizacionhoyar').and_return(official_tweet)
|
24
|
-
end
|
25
|
-
|
26
|
-
describe '#new' do
|
27
|
-
it 'fails if argument is not a Configuration class' do
|
28
|
-
expect { described_class.new(nil) }.to raise_error(ArgumentError)
|
29
|
-
end
|
30
|
-
|
31
|
-
it 'start as invalid right after creation' do
|
32
|
-
subject.should_not be_valid
|
33
|
-
end
|
34
|
-
|
35
|
-
it 'fails if you try to use it before is valid' do
|
36
|
-
expect { subject.gap }.to raise_error
|
37
|
-
expect { subject.gap_percent }.to raise_error
|
38
|
-
expect { subject.output }.to raise_error
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
context 'updated' do
|
43
|
-
before do
|
44
|
-
subject.update!
|
45
|
-
end
|
46
|
-
|
47
|
-
describe '#update!' do
|
48
|
-
it 'becomes valid after calling update!' do
|
49
|
-
subject.should be_valid
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
describe '#output' do
|
54
|
-
it 'has an output with real values' do
|
55
|
-
subject.output.should match(/Blue/)
|
56
|
-
subject.output.should match(/Tarjeta/)
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
describe 'gap values' do
|
61
|
-
it 'has a valid gap card value' do
|
62
|
-
subject.gap_card.should be >= 0
|
63
|
-
end
|
64
|
-
|
65
|
-
it 'has a valid gap card % value' do
|
66
|
-
subject.gap_card_percent.should be >= 0
|
67
|
-
end
|
68
|
-
|
69
|
-
it 'has a valid gap official value' do
|
70
|
-
subject.gap_official.should be >= 0
|
71
|
-
end
|
72
|
-
|
73
|
-
it 'has a valid gap official % value' do
|
74
|
-
subject.gap_official_percent.should be >= 0
|
75
|
-
end
|
3
|
+
vcr_options = { allow_playback_repeats: true }
|
4
|
+
|
5
|
+
describe Dolarblue, vcr: vcr_options do
|
6
|
+
context 'Class Methods' do
|
7
|
+
subject { Dolarblue }
|
8
|
+
|
9
|
+
it 'should return all the dollar exchange Blue/Official/Card values and percentiles suitable for user printing' do
|
10
|
+
# expect(subject.get_output).to match(/Dollar Blue.*\d+\.\d+.*^Information source/)
|
11
|
+
expect(subject.get_output).to match(/Obtaining/)
|
12
|
+
expect(subject.get_output).to match(/Done/)
|
13
|
+
values = %q{\d+\.\d+\s+\/\s+\d+\.\d+}
|
14
|
+
expect(subject.get_output).to match(/Official.*#{values}/)
|
15
|
+
expect(subject.get_output).to match(/Card.*n\/a\s+\/\s+\d+\.\d+/)
|
16
|
+
expect(subject.get_output).to match(/Blue.*#{values}/)
|
17
|
+
expect(subject.get_output).to match(/Gap card/)
|
18
|
+
expect(subject.get_output).to match(/Gap official/)
|
19
|
+
expect(subject.get_output).to match(/Information source/)
|
76
20
|
end
|
77
21
|
end
|
78
|
-
|
79
|
-
describe 'constants' do
|
80
|
-
it 'should have a version number' do
|
81
|
-
Dolarblue::VERSION.should_not be_nil
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
describe 'ClassMethods' do
|
86
|
-
it 'has an get_output() class method to retrieve latest values' do
|
87
|
-
Dolarblue.get_output.should match(/Blue/)
|
88
|
-
Dolarblue.get_output.should match(/Tarjeta/)
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
22
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,22 +1,32 @@
|
|
1
|
-
|
2
|
-
require '
|
1
|
+
unless %w(jruby rbx).include? RUBY_ENGINE
|
2
|
+
require 'simplecov'
|
3
|
+
require 'coveralls'
|
3
4
|
|
4
|
-
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
5
|
-
|
6
|
-
|
7
|
-
]
|
8
|
-
SimpleCov.start
|
5
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
6
|
+
SimpleCov::Formatter::HTMLFormatter,
|
7
|
+
Coveralls::SimpleCov::Formatter
|
8
|
+
]
|
9
|
+
SimpleCov.start
|
10
|
+
end
|
9
11
|
|
10
|
-
# Internal
|
11
12
|
require 'dolarblue'
|
12
13
|
|
14
|
+
require 'vcr' # gem 'vcr'
|
15
|
+
|
16
|
+
VCR.configure do |c|
|
17
|
+
c.cassette_library_dir = 'spec/cassettes'
|
18
|
+
c.hook_into :webmock
|
19
|
+
c.configure_rspec_metadata!
|
20
|
+
end
|
21
|
+
|
13
22
|
# Require this file using `require "spec_helper"` within each of your specs
|
14
23
|
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
15
24
|
RSpec.configure do |config|
|
16
25
|
config.treat_symbols_as_metadata_keys_with_true_values = true
|
17
26
|
config.run_all_when_everything_filtered = true
|
18
27
|
config.filter_run :focus
|
19
|
-
|
20
|
-
|
21
|
-
|
28
|
+
config.order = 'random' # Run specs in random order to surface order dependencies.
|
29
|
+
config.expect_with :rspec do |c|
|
30
|
+
c.syntax = :expect # disable `should` syntax http://goo.gl/BGxqP
|
31
|
+
end
|
22
32
|
end
|