dolarblue 0.3.0 → 0.4.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,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
- # Returns current object state, whether valid or not
18
- #
19
- # @return [true, false] boolean state
20
- def valid?
21
- @blue.valid? && @official.valid?
22
- end
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
- # Connect to the source and retrieve dollar exchange values
25
- #
26
- # @return [self]
27
- def update!
28
- print "Obtaining latest AR$ vs US$ exchange values..."
29
- @blue.update!
30
- @official.update!
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
- # Returns the gap between the real (blue) dollar value versus the official
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
- # Returns the gap percentile 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 * 100).round(0)
48
- end
36
+ log "Parsing values..."
37
+ parse_values Nokogiri::HTML(html_file)
49
38
 
50
- def card_fee_sell_value
51
- (@official.sell_value * @card_fee).round(2)
52
- end
39
+ log "\nDone: #{Time.now.localtime}\n"
40
+ self
41
+ end
53
42
 
54
- # Returns the gap between the real (blue) dollar value versus the dollar "tarjeta" vale
55
- #
56
- # @return [Float] percentile value between 0..1
57
- def gap_card
58
- 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
59
- (@blue.sell_value / card_fee_sell_value - 1)
60
- end
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
- # Returns the gap percentile between the real (blue) dollar value versus the official
63
- #
64
- # @return [Float] percentile value between 0..100
65
- def gap_card_percent
66
- (gap_card * 100).round(0)
67
- end
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
- # Return a string suitable for user output about dollar "tarjeta" values
70
- #
71
- # @return [String] output exchange values 1 line string
72
- def card_output_values
73
- %Q{- Dolar "Tarjeta": n/a / #{'%.2f' % card_fee_sell_value} (Updated #{@official.updated_ago})}
74
- end
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
- # Output string to be used by the binary `dolarblue`
77
- #
78
- # @return [String] the output with dollar exchange information
79
- def output
80
- <<-OUTPUT
81
- #{@official.output_values}
82
- #{card_output_values}
83
- #{@blue.output_values}
84
- - Gap "tarjeta"..: #{gap_card_percent}%
85
- - Gap (official).: #{gap_official_percent}%
86
-
87
- Information sources:
88
- #{@official.output_link}
89
- #{@blue.output_link}
90
- OUTPUT
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
@@ -0,0 +1,7 @@
1
+ require 'dolarblue/xchange'
2
+
3
+ class Dolarblue
4
+ # Class used to hold sell/buy values functionality for dollar "Official"
5
+ class Official < XChange
6
+ end
7
+ end
@@ -1,3 +1,3 @@
1
1
  class Dolarblue
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -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
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dolarblue::Blue do
4
+ #TODO
5
+ end
File without changes
@@ -1,92 +1,22 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Dolarblue do
4
- let(:blue_tweet) do
5
- Twitter::Tweet.new({
6
- id: '309326405148233731',
7
- text: 'Dolar Paralelo: $7,82',
8
- created_at: DateTime.parse('2013-03-06T12:35:36-03:00'),
9
- screen_name: 'DolarBlue'
10
- })
11
- end
12
- let(:official_tweet) do
13
- Twitter::Tweet.new({
14
- id: '309289043722637312',
15
- text: '#Cotizacion cierre promedio parcial 06-03-13: #Dolar: 5.06 // #Euro: 6.71 // #Real: 2.74.',
16
- created_at: DateTime.parse('2013-03-06T10:07:08-03:00'),
17
- screen_name: 'cotizacionhoyar'
18
- })
19
- end
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
@@ -1,22 +1,32 @@
1
- require 'simplecov'
2
- require 'coveralls'
1
+ unless %w(jruby rbx).include? RUBY_ENGINE
2
+ require 'simplecov'
3
+ require 'coveralls'
3
4
 
4
- SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
5
- SimpleCov::Formatter::HTMLFormatter,
6
- Coveralls::SimpleCov::Formatter
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
- # Run specs in random order to surface order dependencies.
21
- config.order = 'random'
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