halffare 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bda0442ac9dab0587de36dde13f84c71f1c45753
4
+ data.tar.gz: 16e0a906faeda411f7a5b4bd6b57ffc38eae4d2d
5
+ SHA512:
6
+ metadata.gz: f2c60bf02b70ce2f8d9fcc65f07461bd84f9ff50ae694bb2e6da28625313ed2b1335f312861bef01c5f1c6fa15ff24ef7dab1283453493a1f5b6160289f11373
7
+ data.tar.gz: 908e0da3000e7155e1c1ce3b7da14ce772a3d4fca00860939667871fc07541607baa71e7761b1b8943c517b84c8eafe0decd8f785cd373a10a99b1e427410ce3
@@ -0,0 +1,38 @@
1
+ = halffare
2
+ --------------------------------------
3
+ This software is in no way affiliated with, authorized, maintained, sponsored or endorsed by sbb.ch or any of its affiliates or subsidiaries.
4
+ --------------------------------------
5
+
6
+ +halffare+ evaluates whether a SBB Half-Fare travelcard is profitable based on your online order history.
7
+
8
+ Provides commands to download your online order history from sbb.ch and evaluate it.
9
+
10
+ {<img src="https://raw.githubusercontent.com/rndstr/halffare/master/media/halffare-results.png" alt="halffare evaluation results" />}
11
+
12
+
13
+ == Getting started
14
+
15
+ Install
16
+
17
+ $ gem install halffare
18
+
19
+ Download order history
20
+
21
+ $ halffare fetch --months=12 --output=year.halffare
22
+
23
+ Evaluate orders
24
+
25
+ $ halffare stats --input=year.halffare
26
+
27
+ More options
28
+
29
+ $ halffare help
30
+
31
+ == Usage
32
+
33
+ :include:halffare.rdoc
34
+
35
+ == License
36
+
37
+ {MIT license}[https://github.com/rndstr/halffare/blob/master/LICENSE]
38
+
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env ruby
2
+ require 'gli'
3
+ require 'halffare'
4
+
5
+ include GLI::App
6
+
7
+ program_desc 'Evaluates whether a SBB Half-Fare travelcard is profitable based on your online order history'
8
+
9
+ version Halffare::VERSION
10
+
11
+ desc 'Fetches your order history from sbb.ch and stores them in a file'
12
+ command :fetch do |c|
13
+ c.desc 'Username for sbb.ch'
14
+ c.arg_name 'username'
15
+ c.flag [:u,:username]
16
+
17
+ c.desc 'Password for sbb.ch'
18
+ c.arg_name 'password'
19
+ c.flag [:p,:password]
20
+
21
+ c.desc 'Filename to store the data in'
22
+ c.default_value 'orders.halffare'
23
+ c.arg_name 'output'
24
+ c.flag [:o, :output]
25
+
26
+ c.desc 'Number of pages to retrieve'
27
+ c.default_value 32
28
+ c.arg_name 'count'
29
+ c.flag [:pages]
30
+
31
+ c.desc 'Stop fetching orders when reaching months back'
32
+ c.arg_name :months
33
+ c.flag [:m, :months]
34
+
35
+ c.desc 'Overwrite an existing data file'
36
+ c.switch :force, :negatable => false
37
+
38
+ c.desc 'Also print debug output'
39
+ c.switch :debug, :negatable => false
40
+
41
+ c.action do |global_options,options,args|
42
+ Halffare.debug = options[:debug]
43
+ if File.exists?(options[:output]) && !options[:force]
44
+ log_error "file #{options[:output]} exists, use --force or --output"
45
+ else
46
+ fetch = Halffare::Fetch.new()
47
+ fetch.login(options[:username], options[:password])
48
+ fetch.download(options[:output], options[:pages], options[:months])
49
+ end
50
+ end
51
+ end
52
+
53
+ desc "Calculates and displays stats about your order history"
54
+ command :stats do |c|
55
+ c.desc 'Filename to read the data from'
56
+ c.default_value 'orders.halffare'
57
+ c.arg_name 'input'
58
+ c.flag [:i, :input]
59
+
60
+ c.desc 'Strategy to use for determining price savings [guess, sbb, sbbguess]'
61
+ c.default_value :sbbguess
62
+ c.arg_name :strategy
63
+ c.flag [:s, :strategy]
64
+ # c.must_match [:sbb, :guess, 'sbbguess']
65
+
66
+ c.desc 'Prices found in input file are of type [half, full, ask]'
67
+ c.default_value :ask
68
+ c.arg_name :pricetype
69
+ c.flag [:p, :pricetype]
70
+ # c.must_match [:half, :full, :ask]
71
+
72
+ c.desc 'Restrict how many months in the past to consider orders'
73
+ c.arg_name :months
74
+ c.flag [:m, :months]
75
+
76
+ c.desc 'Also print debug output'
77
+ c.switch :debug, :negatable => false
78
+
79
+ c.action do |global_options,options,args|
80
+ Halffare.debug = options[:debug]
81
+ stats = Halffare::Stats.new
82
+ stats.read(options[:input], options[:months])
83
+ if stats.count > 0
84
+ if options[:pricetype] == :ask
85
+ options[:pricetype] = yesno('Did you buy your tickets with a half-fare card?', nil) ? 'half' : 'full';
86
+ end
87
+ stats.calculate(options[:strategy], options[:pricetype] == 'half')
88
+ stats.display(options[:pricetype] == 'half')
89
+ end
90
+ end
91
+ end
92
+
93
+ pre do |global,command,options,args|
94
+ # Pre logic here
95
+ # Return true to proceed; false to abort and not call the
96
+ # chosen command
97
+ # Use skips_pre before a command to skip this block
98
+ # on that command only
99
+ true
100
+ end
101
+
102
+ post do |global,command,options,args|
103
+ # Post logic here
104
+ # Use skips_post before a command to skip this
105
+ # block on that command only
106
+ end
107
+
108
+ on_error do |exception|
109
+ p exception
110
+ p exception.backtrace.join("\n")
111
+ # Error logic here
112
+ # return false to skip default error handling
113
+ true
114
+ end
115
+
116
+ exit run(ARGV)
@@ -0,0 +1,114 @@
1
+ == halffare - Evaluates whether a SBB Half-Fare travelcard is profitable based on your online order history
2
+
3
+ v0.1.1
4
+
5
+ === Global Options
6
+ === --help
7
+ Show this message
8
+
9
+
10
+
11
+ === --version
12
+ Display the program version
13
+
14
+
15
+
16
+ === Commands
17
+ ==== Command: <tt>fetch </tt>
18
+ Fetches your order history from sbb.ch and stores them in a file
19
+
20
+
21
+ ===== Options
22
+ ===== -m|--months months
23
+
24
+ Stop fetching orders when reaching months back
25
+
26
+ [Default Value] None
27
+
28
+
29
+ ===== -o|--output output
30
+
31
+ Filename to store the data in
32
+
33
+ [Default Value] orders.halffare
34
+
35
+
36
+ ===== -p|--password password
37
+
38
+ Password for sbb.ch
39
+
40
+ [Default Value] None
41
+
42
+
43
+ ===== --pages count
44
+
45
+ Number of pages to retrieve
46
+
47
+ [Default Value] 32
48
+
49
+
50
+ ===== -u|--username username
51
+
52
+ Username for sbb.ch
53
+
54
+ [Default Value] None
55
+
56
+
57
+ ===== --debug
58
+ Also print debug output
59
+
60
+
61
+
62
+ ===== --force
63
+ Overwrite an existing data file
64
+
65
+
66
+
67
+ ==== Command: <tt>help command</tt>
68
+ Shows a list of commands or help for one command
69
+
70
+ Gets help for the application or its commands. Can also list the commands in a way helpful to creating a bash-style completion function
71
+ ===== Options
72
+ ===== -c
73
+ List commands one per line, to assist with shell completion
74
+
75
+
76
+
77
+ ==== Command: <tt>stats </tt>
78
+ Calculates and displays stats about your order history
79
+
80
+
81
+ ===== Options
82
+ ===== -i|--input input
83
+
84
+ Filename to read the data from
85
+
86
+ [Default Value] orders.halffare
87
+
88
+
89
+ ===== -m|--months months
90
+
91
+ Restrict how many months in the past to consider orders
92
+
93
+ [Default Value] None
94
+
95
+
96
+ ===== -p|--pricetype pricetype
97
+
98
+ Prices found in input file are of type [half, full, ask]
99
+
100
+ [Default Value] ask
101
+
102
+
103
+ ===== -s|--strategy strategy
104
+
105
+ Strategy to use for determining price savings [guess, sbb, sbbguess]
106
+
107
+ [Default Value] sbbguess
108
+
109
+
110
+ ===== --debug
111
+ Also print debug output
112
+
113
+
114
+
@@ -0,0 +1,69 @@
1
+ require 'mechanize'
2
+ require 'highline/import'
3
+ require 'logger'
4
+ require 'date'
5
+ require 'yaml'
6
+
7
+ require 'halffare/version.rb'
8
+ require 'halffare/fetch.rb'
9
+ require 'halffare/stats.rb'
10
+ require 'halffare/model/order.rb'
11
+ require 'halffare/price.rb'
12
+ require 'halffare/price_base.rb'
13
+ require 'halffare/price_guess.rb'
14
+ require 'halffare/price_sbb.rb'
15
+
16
+ module Kernel
17
+ private
18
+ def currency(cash)
19
+ 'CHF' + sprintf('%.2f', cash.to_f.abs)
20
+ end
21
+
22
+ def log_debug(str)
23
+ say(str) if Halffare.debug
24
+ end
25
+
26
+ def log_info(str)
27
+ say("#{str}")
28
+ end
29
+
30
+ def log_notice(str)
31
+ say("<%= color('#{str.gsub("'","\\\\'")}', BOLD) %>")
32
+ end
33
+
34
+ def log_heading(str)
35
+ say("<%= color('#{str.gsub("'","\\\\'")}', YELLOW, BOLD) %>")
36
+ end
37
+
38
+ def log_result(str)
39
+ say("<%= color('#{str.gsub("'","\\\\'")}', GREEN) %>")
40
+ end
41
+
42
+ def log_emphasize(str)
43
+ say("<%= color('#{str.gsub("'","\\\\'")}', CYAN) %>")
44
+ end
45
+
46
+ def log_error(str)
47
+ say("<%= color('#{'ERROR: ' + str.gsub("'","\\\\'")}', WHITE, ON_RED, BOLD) %>")
48
+ end
49
+
50
+ def log_order(order)
51
+ log_info "\n"
52
+ log_heading "#{order.order_date} #{order.description}"
53
+ log_emphasize "You paid: #{currency(order.price)}"
54
+ end
55
+
56
+ def yesno(prompt = 'Continue?', default = true)
57
+ a = ''
58
+ s = '[y/n]'
59
+ unless default.nil?
60
+ s = default ? '[Y/n]' : '[y/N]'
61
+ end
62
+ d = default ? 'y' : 'n'
63
+ until %w[y n].include? a
64
+ a = ask("#{prompt} #{s} ") { |q| q.limit = 1; q.case = :downcase }
65
+ a = d if a.length == 0
66
+ end
67
+ a == 'y'
68
+ end
69
+ end
@@ -0,0 +1,112 @@
1
+ module Halffare
2
+
3
+ class Fetch
4
+ USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11'
5
+ URL_LOGIN = 'http://www.sbb.ch/meta/login.html'
6
+ URL_ORDERS = 'https://www.sbb.ch/ticketshop/b2c/dossierListe.do'
7
+ ORDER_NOTE_FILE_CREATED = 'halffare-orders-file-created'
8
+
9
+ def initialize()
10
+ @agent = ::Mechanize.new
11
+ @agent.user_agent = USER_AGENT
12
+ end
13
+
14
+ def login(username, password)
15
+ username = ask("sbb.ch username? ") unless username
16
+ password = ask("sbb.ch password? ") { |q| q.echo = "*" } unless password
17
+ log_info "logging in..."
18
+
19
+ #@agent.log = Logger.new('debug.log')
20
+
21
+ @response = @agent.get URL_LOGIN
22
+
23
+ login = @response.forms.first
24
+ login['logon.username'] = username
25
+ login['logon.password'] = password
26
+
27
+ @response = @agent.submit(login)
28
+ end
29
+
30
+ def download(filename, pages, months)
31
+ @response = @agent.get URL_ORDERS
32
+
33
+ logged_in!
34
+
35
+ begin
36
+ left = pages.to_i
37
+ page = 1
38
+ stop_after = months ? (Date.today << months.to_i).strftime('%Y-%m-%d') : nil
39
+
40
+ log_info "will stop scraping after reaching #{stop_after}" unless stop_after.nil?
41
+ log_info "writing data to #{filename}"
42
+
43
+ file = File.open(filename, "w")
44
+ oldest_date_on_page = Date.today.strftime
45
+
46
+ # fake entry so when evaluating this file the calculation goes up until today
47
+ # and not to the last order.
48
+ file.write "#{oldest_date_on_page}|#{oldest_date_on_page}|0|#{ORDER_NOTE_FILE_CREATED}|\n"
49
+
50
+ loop do
51
+ print ">>> page #{page}/#{pages} "
52
+ orders do |idx, travel_date, order_date, price, note, name|
53
+ print "."
54
+
55
+ travel_date = Date.parse(travel_date).strftime
56
+ order_date = Date.parse(order_date).strftime
57
+ oldest_date_on_page = [order_date, oldest_date_on_page].min
58
+
59
+ file.write "#{travel_date}|#{order_date}|#{price}|#{note}|#{name}\n" if stop_after.nil? || travel_date >= stop_after
60
+ end
61
+ puts
62
+ next!
63
+
64
+ log_debug "oldest order on page was on #{oldest_date_on_page}" if Halffare.debug
65
+
66
+ page += 1
67
+ left -= 1
68
+
69
+ break if !stop_after.nil? && oldest_date_on_page < stop_after
70
+ break if left <= 0
71
+ end
72
+ rescue IOError => e
73
+ puts e
74
+ ensure
75
+ file.close unless file == nil
76
+ end
77
+ end
78
+
79
+ private
80
+ def orders
81
+ @response.search('#orders tbody tr').each do |order|
82
+ idx = order.attr('id').split('-')[1]
83
+ order_date = order.xpath('./td[1]').text.gsub(/[[:space:]]/, ' ').strip
84
+ travel_date = order.xpath('./td[2]').text.gsub(/[[:space:]]/, ' ').strip
85
+ price = order.xpath('./td[4]').text.gsub(/[[:space:]]/, ' ').strip
86
+ note = order.xpath('./td[5]').text.gsub(/[[:space:]]/, ' ').strip
87
+
88
+ description = ""
89
+ @response.search('#bezeichnungen tr#ordersBezeichnung-' + idx + ' td ul li').each { |i|
90
+ description << "::" unless description.empty?
91
+
92
+ description << i.to_s.gsub(/<br>.*</, '<').gsub(/[[:space:]]/, ' ').gsub(/<.*?>/, '').strip
93
+ }
94
+
95
+ yield idx, order_date, travel_date, price, note, description if block_given?
96
+ end
97
+ end
98
+
99
+ def next!
100
+ @response = @response.forms_with(:action => '/ticketshop/b2c/dossierListe.do').first
101
+ @response = @response.submit(@response.button_with(:name => 'method:more'))
102
+ end
103
+
104
+ def logged_in!
105
+ error = @response.at '.skinMessageBoxError'
106
+ if error != nil
107
+ STDERR.puts "ERROR: Not logged in, verify your username and password"
108
+ exit 1
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,16 @@
1
+ module Halffare
2
+ module Model
3
+ class Order
4
+ attr_reader :travel_date, :order_date, :price, :note, :description
5
+ def initialize(row)
6
+ from_row(row)
7
+ end
8
+
9
+ def from_row(row)
10
+ @travel_date, @order_date, @price, @note, @description = row.split("|")
11
+ @description.strip!
12
+ @price = @price.to_f
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ module Halffare
2
+ class Price
3
+ def initialize (strategy)
4
+ @strategy = strategy
5
+ end
6
+
7
+ def get(order)
8
+ @strategy.get(order)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,27 @@
1
+ module Halffare
2
+ class PriceBase
3
+
4
+ # Extracts the prices from an order.
5
+ #
6
+ # @param order [Order] The order
7
+ # @return halfprice, fullprice
8
+ def get(order)
9
+ raise 'you need to implement get(order) in your price strategy'
10
+ end
11
+
12
+ def halffare=(hf)
13
+ @halffare = hf
14
+ end
15
+
16
+ private
17
+ # Which price has been paid
18
+ def price_paid
19
+ @halffare ? 'half' : 'full'
20
+ end
21
+
22
+ # Opposite of {#price_paid}
23
+ def price_paid_other
24
+ @halffare ? 'full' : 'half'
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,14 @@
1
+ module Halffare
2
+ # Simple guesstimator assuming half-fare card is always half the price
3
+ # which is probably the case for non-regional tickets
4
+ class PriceGuess < PriceBase
5
+ def get(order)
6
+ price = order.price.to_f
7
+ if @halffare
8
+ return price, price*2
9
+ else
10
+ return price/2, price
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,102 @@
1
+ # encoding: binary
2
+ module Halffare
3
+ # More accurate price strategy
4
+ #
5
+ # Uses a static table for price conversions, falling back on
6
+ # guess strategy if no entry found
7
+ class PriceSbb < PriceGuess
8
+ alias :price_guess_get :get
9
+
10
+ # @param force_guess_fallback [true, false] To use `guess` strategy as fallback instead of asking the user
11
+ def initialize(force_guess_fallback = false)
12
+ @force_guess_fallback = force_guess_fallback
13
+ end
14
+
15
+ def get(order)
16
+ log_debug "looking for an existing rule..."
17
+ rules.each { |rule|
18
+ rule.each { |match, definition|
19
+ if order.description =~ /#{match}/u
20
+ case
21
+ when definition['scale'] != nil
22
+ log_debug "found scale rule: #{definition}"
23
+ return price_scale(definition, order)
24
+ when definition['set'] != nil
25
+ log_debug "found set rule: #{definition}"
26
+ return price_set(definition, order)
27
+ when definition['choices'] != nil
28
+ log_debug "found choices rules: #{definition}"
29
+ return price_choices(definition, order)
30
+ end
31
+ end
32
+ }
33
+ }
34
+ log_result "no satisfying match found" if Halffare.debug
35
+ return price_guess_get(order) if @force_guess_fallback
36
+
37
+ ask_for_price(order)
38
+ end
39
+
40
+ private
41
+ # Loads rules form the rules file.
42
+ def rules
43
+ @rules ||= YAML.load_file(File.expand_path('../pricerules_sbb.yml', __FILE__))
44
+ end
45
+
46
+ # Calculates the prices according to the `scale` definition.
47
+ def price_scale(definition, order)
48
+ return definition['scale'][price_paid][0] * order.price, definition['scale'][price_paid][1] * order.price
49
+ end
50
+
51
+ # Calculates the prices according to the `set` definition.
52
+ def price_set(definition, order)
53
+ if order.price != definition['set'][price_paid]
54
+ log_order(order)
55
+ log_error 'order matched but price differs; ignoring this order.'
56
+ p order
57
+ p definition
58
+ return 0, 0
59
+ else
60
+ return definition['set']['half'], definition['set']['full']
61
+ end
62
+ end
63
+
64
+ # Calculates the prices according to the `choices` definition.
65
+ def price_choices(definition, order)
66
+ # auto select
67
+ definition['choices'].each { |name,prices| return prices['half'], prices['full'] if prices[price_paid] == order.price }
68
+
69
+ return price_guess_get(order) if @force_guess_fallback
70
+
71
+ choose do |menu|
72
+ menu.prompt = "\nSelect the choice that applies to your travels? "
73
+
74
+ log_info "\n"
75
+ definition['choices'].each do |name,prices|
76
+ menu.choice "#{name} (half-fare: #{currency(prices['half'])}, full: #{currency(prices['full'])})" do
77
+ end
78
+ end
79
+ menu.choice "…or enter manually" do ask_for_price(order) end
80
+ end
81
+ end
82
+
83
+ # Ask the user for the price.
84
+ def ask_for_price(order)
85
+ guesshalf, guessfull = price_guess_get(order)
86
+
87
+ if !Halffare.debug
88
+ # was already logged
89
+ log_order(order)
90
+ end
91
+
92
+ if @halffare
93
+ other = ask("What would have been the full price? ", Float) { |q| q.default = guessfull }
94
+ return order.price, other
95
+ else
96
+ other = ask("What would have been the half-fare price? ", Float) { |q| q.default = guesshalf }
97
+ return other, order.price
98
+ end
99
+ end
100
+
101
+ end
102
+ end
@@ -0,0 +1,16 @@
1
+ - "Gutschrift:Erstattung":
2
+ scale: {half: [-1,-2], full: [-0.5,-1]}
3
+ - "Individual tickets":
4
+ scale: {half: [1,2], full: [0.5,1]}
5
+ - "bike pass":
6
+ scale: {half: [1,1], full: [1,1]}
7
+ - "ZVV Individual ticket/Day Pass city network Zürich \\(Zone 110\\)":
8
+ set: {half: 5.80, full: 8.40}
9
+ - "Passepartout Individual ticket LUZERN":
10
+ choices: {
11
+ "Kurzstrecke 101": {half: 2.40, full: 2.90},
12
+ "2 Zonen 101": {half: 3.20, full: 4.50}
13
+ }
14
+ - "Class upgrade \\(route-specific\\)":
15
+ scale: {half: [1,2], full: [0.5,1]}
16
+
@@ -0,0 +1,155 @@
1
+ module Halffare
2
+ class Stats
3
+
4
+ # Reads orders from `filename` that date back to max `months` months.
5
+ #
6
+ # @param filename [String] The filename to read from
7
+ # @param months [Integer, nil] Number of months look back or nil for all
8
+ def read(filename, months=nil)
9
+ @orders = []
10
+ start = months ? (Date.today << months.to_i).strftime('%Y-%m-%d') : nil
11
+ file = File.open(filename, "r:UTF-8") do |f|
12
+ while line = f.gets
13
+ order = Halffare::Model::Order.new(line)
14
+ if (start.nil? || line[0,10] >= start) && (order.note != Fetch::ORDER_NOTE_FILE_CREATED)
15
+ @orders.push(order)
16
+ end
17
+ end
18
+ end
19
+ log_info "read #{@orders.length} orders from #{filename}"
20
+ if @orders.length == 0
21
+ if start.nil?
22
+ log_notice "no orders found"
23
+ else
24
+ log_notice "no orders found after #{start}, maybe tweak the --months param"
25
+ end
26
+ end
27
+ end
28
+
29
+ # How many orders were processed.
30
+ def count
31
+ @orders.length
32
+ end
33
+
34
+ # Calculates prices according to given strategy.
35
+ #
36
+ # @param strategy [String] Strategy name
37
+ # @param halffare [true, false] True if tickets were bought with a halffare card
38
+ def calculate(strategy, halffare)
39
+ @halfprice = 0
40
+ @fullprice = 0
41
+
42
+ if halffare
43
+ log_info "assuming order prices as half-fare"
44
+ else
45
+ log_info "assuming order prices as full"
46
+ end
47
+
48
+ log_notice "please note that you are using a strategy that involves guessing the real price" if ['guess', 'sbbguess'].include? strategy
49
+
50
+ strategy = price_factory(strategy)
51
+ strategy.halffare = halffare
52
+ log_info "using price strategy: #{strategy.class}"
53
+ price = Price.new(strategy)
54
+ log_info "calculating prices..."
55
+
56
+ @date_min = false
57
+ @date_max = false
58
+ @orders.each do |order|
59
+
60
+ if Halffare.debug
61
+ log_order(order)
62
+ end
63
+
64
+ halfprice, fullprice = price.get(order)
65
+
66
+ if Halffare.debug
67
+ if halfprice != 0 && fullprice != 0
68
+ log_result "FOUND: #{order.description} (#{order.price}): half=#{currency(halfprice)}, full=#{currency(fullprice)}"
69
+
70
+ if halffare
71
+ log_emphasize "You would pay (full price): #{currency(fullprice)}, you save #{currency(fullprice - order.price)}"
72
+ else
73
+ log_emphasize "You would pay (half-fare): #{currency(halfprice)}, you pay #{currency(order.price - halfprice)} more"
74
+ end
75
+ end
76
+ end
77
+
78
+ @halfprice += halfprice
79
+ @fullprice += fullprice
80
+
81
+ @date_min = order.travel_date if !@date_min || order.travel_date < @date_min
82
+ @date_max = order.travel_date if !@date_max || order.travel_date > @date_max
83
+ end
84
+ end
85
+
86
+ # Load config file.
87
+ def config
88
+ @config ||= YAML.load_file(File.expand_path('../../../halffare.yml', __FILE__))
89
+ end
90
+
91
+ def display(halffare)
92
+ say("\n")
93
+ days = (Date.parse(@date_max) - Date.parse(@date_min)).to_i
94
+ paid = halffare ? @halfprice : @fullprice
95
+ paid_per_day = paid / days
96
+ fullprice_per_day = @fullprice / days
97
+ saved_per_day = (@fullprice - @halfprice) / days
98
+
99
+ log_info "Results"
100
+ log_info "======="
101
+ say("\n")
102
+ log_info "OVERALL"
103
+ log_info "orders: #{@orders.length}"
104
+ log_info "first travel date: #{@date_min}"
105
+ log_info "last travel date : #{@date_max} (#{days} days)"
106
+ log_info "half-fare price : #{currency(@halfprice)}#{halffare ? ' (what you paid)':''}"
107
+ log_info "full price : #{currency(@fullprice)}#{halffare ? '' : ' (what you paid)'}"
108
+ log_info "half-fare savings: #{currency(@fullprice - @halfprice)}"
109
+ say("\n")
110
+ log_info "PER DAY"
111
+ log_info "you pay : #{currency(paid_per_day)}"
112
+ log_info "half-fare savings: #{currency(saved_per_day)}"
113
+
114
+ say("\n")
115
+ log_info "Half-Fare Card"
116
+ log_info "-------------"
117
+
118
+ config['cards'].each do |months,cash|
119
+ say("\n")
120
+ years = months/12
121
+ saved = saved_per_day * years * 365
122
+ log_info "#{years} YEAR#{years == 1 ? '' : 'S'} #{currency(cash)}"
123
+ log_info "you pay : #{currency(paid_per_day * years * 365)}"
124
+ log_info "full price : #{currency(fullprice_per_day * years * 365)}"
125
+ log_info "half-fare savings: #{currency(saved)}"
126
+ if saved >= cash
127
+ say("<%= color('GOOD TO GO', WHITE, BOLD, ON_GREEN) %> (#{currency((saved - cash) / years)} net win per year)")
128
+ else
129
+ say("<%= color('NAY', WHITE, BOLD, ON_RED) %> (#{currency((cash - saved) / years)} net loss per year)")
130
+ end
131
+ end
132
+
133
+ if fullprice_per_day * 365 > 2500
134
+ say("\n")
135
+ log_result "Since your tickets would cost approximately #{currency(fullprice_per_day * 365)} per year (w/o half-fare card) you should check out the GA prices for your age bracket: http://www.sbb.ch/abos-billette/abonnemente/ga/preise.html"
136
+ end
137
+ end
138
+
139
+ private
140
+ def price_factory(strategy)
141
+ case strategy.to_s
142
+ when "guess"
143
+ Halffare::PriceGuess.new
144
+ when "sbb"
145
+ Halffare::PriceSbb.new
146
+ when "sbbguess"
147
+ Halffare::PriceSbb.new(true)
148
+ else
149
+ raise "unknown strategy: #{strategy}"
150
+ end
151
+ end
152
+
153
+ end
154
+ end
155
+
@@ -0,0 +1,11 @@
1
+ module Halffare
2
+ VERSION = '0.1.1'
3
+ @@debug = false
4
+
5
+ def self.debug=(d)
6
+ @@debug = d
7
+ end
8
+ def self.debug
9
+ @@debug
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,150 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: halffare
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Roland Schilter
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-05-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rdoc
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: aruba
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: gli
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: 2.9.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '='
67
+ - !ruby/object:Gem::Version
68
+ version: 2.9.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: mechanize
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '='
74
+ - !ruby/object:Gem::Version
75
+ version: 2.7.3
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '='
81
+ - !ruby/object:Gem::Version
82
+ version: 2.7.3
83
+ - !ruby/object:Gem::Dependency
84
+ name: highline
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '='
88
+ - !ruby/object:Gem::Version
89
+ version: 1.6.21
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '='
95
+ - !ruby/object:Gem::Version
96
+ version: 1.6.21
97
+ description: Evaluates whether a SBB Half-Fare travelcard is profitable based on your
98
+ online order history
99
+ email: roli@schilter.me
100
+ executables:
101
+ - halffare
102
+ extensions: []
103
+ extra_rdoc_files:
104
+ - README.rdoc
105
+ - halffare.rdoc
106
+ files:
107
+ - bin/halffare
108
+ - lib/halffare/model/order.rb
109
+ - lib/halffare/fetch.rb
110
+ - lib/halffare/price.rb
111
+ - lib/halffare/price_base.rb
112
+ - lib/halffare/price_guess.rb
113
+ - lib/halffare/price_sbb.rb
114
+ - lib/halffare/pricerules_sbb.yml
115
+ - lib/halffare/stats.rb
116
+ - lib/halffare/version.rb
117
+ - lib/halffare.rb
118
+ - README.rdoc
119
+ - halffare.rdoc
120
+ homepage: https://github.com/rndstr/halffare
121
+ licenses:
122
+ - MIT
123
+ metadata: {}
124
+ post_install_message:
125
+ rdoc_options:
126
+ - --title
127
+ - halffare
128
+ - --main
129
+ - README.rdoc
130
+ - -ri
131
+ require_paths:
132
+ - lib
133
+ - lib
134
+ required_ruby_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - '>='
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ required_rubygems_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - '>='
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ requirements: []
145
+ rubyforge_project:
146
+ rubygems_version: 2.0.5
147
+ signing_key:
148
+ specification_version: 4
149
+ summary: SBB Half-Fare travelcard profitability
150
+ test_files: []