fonecal 0.1.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.
data/fonecal.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'fonecal/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "fonecal"
8
+ spec.version = Fonecal::VERSION
9
+ spec.authors = ["Martin Madsen"]
10
+ spec.email = ["martin2madsen@gmail.com"]
11
+ spec.description = %q{f1cal generates an iCalendar file for the 2014 Formula 1 season.}
12
+ spec.summary = %q{Crawls the Grand Prix list at formula1.com and builds an iCalendar file with events in UTC}
13
+ spec.homepage = "https://github.com/Fapper/f1cal"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake", "~> 10.1"
23
+ spec.add_development_dependency "rspec", "~> 2.14"
24
+
25
+ spec.add_dependency "timezone", "~> 0.3"
26
+ spec.add_dependency "activesupport", "~> 4.0"
27
+ spec.add_dependency 'nokogiri', "~> 1.6"
28
+ spec.add_dependency 'geocoder', "~> 1.1"
29
+ spec.add_dependency 'i18n', "~> 0.6"
30
+ end
@@ -0,0 +1,40 @@
1
+ module Fonecal
2
+ class Calendar
3
+ attr_accessor :calendar
4
+
5
+ def initialize(gps)
6
+ self.calendar = File.new('f1cal.ical', 'w')
7
+
8
+ @calendar.puts "BEGIN:VCALENDAR"
9
+ @calendar.puts "VERSION:2.0"
10
+ @calendar.puts "PRODID:-//fonecal//NONSGML v1.0//EN"
11
+ @calendar.puts ""
12
+
13
+ gps.each do |gp|
14
+ gp.events.each do |event|
15
+ addEvent(gp, event)
16
+ end
17
+ end
18
+
19
+ @calendar.puts "END:VCALENDAR"
20
+ @calendar.close
21
+ end
22
+
23
+ private
24
+
25
+ def addEvent(gp, event)
26
+ @calendar.puts "BEGIN:VEVENT"
27
+
28
+ @calendar.puts "SUMMARY:#{gp.grandPrix}: #{event.name}"
29
+ @calendar.puts "DESCRIPTION:#{gp.raceTitle}: #{event.name}"
30
+ @calendar.puts "DTSTAMP:#{Util.dtToUTC(DateTime.now)}Z"
31
+ @calendar.puts "LOCATION:#{gp.city}, #{gp.country}"
32
+ @calendar.puts "DTSTART:#{Util.dtToUTC(event.start)}Z"
33
+ puts "#{gp.grandPrix}: #{event.name} starts #{event.start} UTC: #{event.start.utc}"
34
+ @calendar.puts "DTEND:#{Util.dtToUTC(event.end)}Z"
35
+
36
+ @calendar.puts "END:VEVENT"
37
+ @calendar.puts ""
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,12 @@
1
+ module Fonecal
2
+ class CalendarCrawler < Crawler
3
+ def eventLinks
4
+ eventLinks = []
5
+
6
+ @site.css('table.raceCalender tr.row').each do |event|
7
+ eventLinks << @root + event.css('td.raceLocation a').attr('href')
8
+ end
9
+ eventLinks
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ require 'fonecal'
2
+
3
+ describe Fonecal::Calendar do
4
+
5
+ end
@@ -0,0 +1,23 @@
1
+ module Fonecal
2
+ class CircuitInfo
3
+ def initialize(info)
4
+ @info = info
5
+ end
6
+
7
+ def name
8
+ if @info['Circuit Name:']
9
+ @info['Circuit Name:'].cap
10
+ else
11
+ ""
12
+ end
13
+ end
14
+
15
+ def city
16
+ @info['City:'].cap
17
+ end
18
+
19
+ def laps
20
+ @info['Number of Laps:'].to_i
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,13 @@
1
+ require 'open-uri'
2
+ require 'nokogiri'
3
+
4
+ module Fonecal
5
+ class Crawler
6
+ attr_accessor :site, :root
7
+
8
+ def initialize(site)
9
+ self.site = Nokogiri::HTML(open(site))
10
+ self.root = Util.getRootUrl(site)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,25 @@
1
+ require 'active_support/all'
2
+
3
+ module Fonecal
4
+ class Event
5
+ attr_accessor :name, :start, :end
6
+
7
+ def initialize(event, timezone)
8
+ # UTC offset in hours, explicitely add +
9
+ if (timezone.utc_offset / 60 / 60) > 0
10
+ offset = "+#{timezone.utc_offset / 60 / 60}"
11
+ else
12
+ offset = offset.to_s
13
+ end
14
+
15
+ self.name = event[:type]
16
+ self.start = DateTime.parse("#{event[:date]}T#{event[:start]} #{offset}")
17
+
18
+ if event[:end]
19
+ self.end = DateTime.parse("#{event[:date]}T#{event[:end]} #{offset}")
20
+ else
21
+ self.end = self.start + 4.hours
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,60 @@
1
+ module Fonecal
2
+ class EventCrawler < Crawler
3
+ def gp
4
+ @site.css('div.raceResultsHeading').text.strip
5
+ end
6
+
7
+ def circuitInfo
8
+ ci = {}
9
+
10
+ @site.css('div.circuitInfoBox table tr').each do |row|
11
+ ci[row.css('td')[0].content.strip] = row.css('td')[1].content.strip
12
+ end
13
+
14
+ city = @site.css('h3.inDetailCircuitName').text
15
+
16
+ ci['City:'] = city
17
+
18
+ return ci
19
+ end
20
+
21
+ def timeTables
22
+ tables = []
23
+ @site.css('div#ctl00_ContentSub_Timetable1_subModuleContentDiv table').each do |tt|
24
+ tables << parseTimeTable(tt)
25
+ end
26
+
27
+ tables
28
+ end
29
+
30
+ private
31
+
32
+ def sessionData(row)
33
+ inf = {}
34
+
35
+ sessData = row.css('td')
36
+ inf[:type] = sessData[0].text.strip
37
+ inf[:start] = sessData[1].css('span')[0].text.strip
38
+ inf[:end] = sessData[1].css('span')[1].text.strip if sessData[1].css('span')[1]
39
+ inf
40
+ end
41
+
42
+ def parseTimeTable(table)
43
+ info = {}
44
+
45
+ rows = table.css 'tr'
46
+
47
+ # The date is the first row in teh table
48
+ info[:date] = rows[0].text.strip
49
+
50
+ # Now extract info from each of the remaining rows
51
+ # Example: Practice 1 12:30 - 14:00 is stored in a row
52
+ info[:sessions] ||= []
53
+ rows[1..-1].each do |row|
54
+ info[:sessions] << sessionData(row)
55
+ end
56
+
57
+ info
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,7 @@
1
+ module Fonecal
2
+ describe Fonecal::Fonecal do
3
+ it "creates an icalendar file" do
4
+ Fonecal.create_ical
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,70 @@
1
+ require 'geocoder'
2
+ require 'timezone'
3
+
4
+ module Fonecal
5
+ class GrandPrix
6
+ attr_accessor :events, :circuit
7
+
8
+ def initialize(eventWebsite)
9
+ @crawler = EventCrawler.new(eventWebsite)
10
+ self.circuit= CircuitInfo.new(@crawler.circuitInfo)
11
+ self.events = []
12
+
13
+ createEvents
14
+ end
15
+
16
+ def location
17
+ if @res ||= Geocoder.search("#{@circuit.name}, #{@circuit.city}").first
18
+ @res
19
+ elsif @res ||= Geocoder.search("#{@circuit.city}").first
20
+ @res
21
+ elsif res ||= Geocoder.search("#{@circuit.name}").first
22
+ @res
23
+ else
24
+ @res
25
+ end
26
+
27
+ #@location ||= Geocoder.search("#{@circuit.name}, #{@circuit.city}").first
28
+ end
29
+
30
+ def city
31
+ @circuit.city
32
+ end
33
+
34
+ def country
35
+ location.country
36
+ end
37
+
38
+ def timezone
39
+ @timezone ||= Timezone::Zone.new :latlon => location.geometry["location"].values
40
+ end
41
+
42
+ def raceTitle
43
+ @crawler.gp.gsub(/\w+/).map(&:capitalize).join(' ')
44
+ end
45
+
46
+ def grandPrix
47
+ "#{country} GP"
48
+ # Belgian Grand Prix
49
+ end
50
+
51
+ private
52
+
53
+ def createEvents
54
+ @crawler.timeTables.each do |day|
55
+ day[:sessions].each do |event|
56
+ type = event[:type]
57
+ event[:date] = day[:date]
58
+
59
+ if(type.include? "Practice")
60
+ @events << Practice.new(event, timezone)
61
+ elsif(type.include? "Qualifying")
62
+ @events << Qualifying.new(event, timezone)
63
+ elsif(type.include? "Race")
64
+ @events << Race.new(event, timezone)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,4 @@
1
+ module Fonecal
2
+ class Practice < Event
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Fonecal
2
+ class Qualifying < Event
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Fonecal
2
+ class Race < Event
3
+ end
4
+ end
@@ -0,0 +1,10 @@
1
+ module Fonecal
2
+ class Session
3
+ attr_accessor :start, :length
4
+
5
+ def initilaize(start, length)
6
+ @start = start
7
+ @length = length
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,26 @@
1
+ require 'timezone'
2
+ require 'active_support/all'
3
+
4
+ module Fonecal
5
+ class Util
6
+ def self.getRootUrl(site)
7
+ site.match('.+\.com').to_s
8
+ end
9
+
10
+ def self.dtToUTC(dt)
11
+ #dt.with_offset(0).strftime("%Y%m%dT%H%M%S")
12
+ dt.utc.strftime("%Y%m%dT%H%M%S")
13
+ end
14
+ end
15
+
16
+ end
17
+
18
+ class String
19
+ def cap
20
+ self.gsub(/\w+/).map(&:capitalize).join(' ')
21
+ end
22
+ end
23
+
24
+ Timezone::Configure.begin do |c|
25
+ c.username = 'fonecal'
26
+ end
@@ -0,0 +1,3 @@
1
+ module Fonecal
2
+ VERSION = "0.1.0"
3
+ end
data/lib/fonecal.rb ADDED
@@ -0,0 +1,25 @@
1
+ require 'fonecal/version'
2
+ require 'fonecal/util'
3
+ require 'fonecal/crawler'
4
+ require 'fonecal/event_crawler'
5
+ require 'fonecal/calendar_crawler'
6
+ require 'fonecal/calendar'
7
+ require 'fonecal/grand_prix'
8
+ require 'fonecal/event'
9
+ require 'fonecal/session'
10
+ require 'fonecal/practice'
11
+ require 'fonecal/qualifying'
12
+ require 'fonecal/race'
13
+ require 'fonecal/circuit_info'
14
+
15
+ module Fonecal
16
+ def self.create_ical
17
+ gps = []
18
+
19
+ Fonecal::CalendarCrawler.new("http://www.formula1.com/races/calendar.html").eventLinks.each do |l|
20
+ gps << Fonecal::GrandPrix.new(l)
21
+ end
22
+
23
+ Fonecal::Calendar.new(gps)
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ require 'fonecal'
2
+
3
+ describe Fonecal::CalendarCrawler do
4
+ let(:crawler) { Fonecal::CalendarCrawler.new("http://www.formula1.com/races/calendar.html") }
5
+
6
+ it "successfully connects to the website" do
7
+ crawler.site.should_not be_nil
8
+ end
9
+
10
+ describe "#eventLinks" do
11
+ it "returns 19 grand prix links" do
12
+ crawler.eventLinks.count.should eql(19)
13
+ end
14
+
15
+ it "has the full url" do
16
+ crawler.eventLinks.first.should eql('http://www.formula1.com/races/in_detail/australia_914/circuit_diagram.html')
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,68 @@
1
+ require 'fonecal'
2
+
3
+ describe Fonecal::EventCrawler do
4
+ let(:crawler) { Fonecal::EventCrawler.new("http://www.formula1.com/races/in_detail/australia_914/circuit_diagram.html") }
5
+
6
+ it "successfully connects to the website" do
7
+ crawler.site.should_not be_nil
8
+ end
9
+
10
+ describe "2014 Australian GP" do
11
+ describe "#circuitInfo" do
12
+ it "fetches a circuit name" do
13
+ crawler.circuitInfo['Circuit Name:'].should eq 'Albert Park'
14
+ end
15
+
16
+ it "fetches a date" do
17
+ crawler.circuitInfo['Race Date:'].should_not be_nil
18
+ end
19
+
20
+ it "fetches a city" do
21
+ crawler.circuitInfo['City:'].should eq 'Melbourne'
22
+ end
23
+ end
24
+
25
+ describe "#gp" do
26
+ it "should fetch the GP name" do
27
+ crawler.gp.should eq '2014 FORMULA 1 ROLEX AUSTRALIAN GRAND PRIX'
28
+ end
29
+ end
30
+
31
+ describe "#timeTables" do
32
+ it "should get three timetables" do
33
+ crawler.timeTables.count.should eq 3
34
+ end
35
+
36
+ it "should have data about practice 1" do
37
+ crawler.timeTables.first[:date].should eq 'Fri 14 March 2014'
38
+ crawler.timeTables.first[:sessions].first[:type].should eq 'Practice 1'
39
+ crawler.timeTables.first[:sessions].first[:start].should eq '12:30'
40
+ end
41
+
42
+ it "should have data about practice 2" do
43
+ crawler.timeTables.first[:date].should eq 'Fri 14 March 2014'
44
+ crawler.timeTables.first[:sessions].last[:type].should eq 'Practice 2'
45
+ crawler.timeTables.first[:sessions].last[:start].should eq '16:30'
46
+ end
47
+
48
+ it "should have data about practice 3" do
49
+ crawler.timeTables[1][:date].should eq 'Sat 15 March 2014'
50
+ crawler.timeTables[1][:sessions].first[:type].should eq 'Practice 3'
51
+ crawler.timeTables[1][:sessions].first[:start].should eq '14:00'
52
+ end
53
+
54
+ it "should have data about qualifying" do
55
+ crawler.timeTables[1][:date].should eq 'Sat 15 March 2014'
56
+ crawler.timeTables[1][:sessions].last[:type].should eq 'Qualifying'
57
+ crawler.timeTables[1][:sessions].last[:start].should eq '17:00'
58
+ end
59
+
60
+ it "should have data about the race" do
61
+ crawler.timeTables.last[:date].should eq 'Sun 16 March 2014'
62
+ crawler.timeTables.last[:sessions].first[:type].should eq 'Race'
63
+ crawler.timeTables.last[:sessions].first[:start].should eq '17:00'
64
+ end
65
+ end
66
+ end
67
+ end
68
+
@@ -0,0 +1,4 @@
1
+ require 'fonecal'
2
+
3
+ describe Fonecal::Event do
4
+ end
@@ -0,0 +1,9 @@
1
+ require 'fonecal'
2
+
3
+ describe Fonecal do
4
+ describe "#create_ical" do
5
+ it "creates the file" do
6
+ Fonecal.create_ical
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,31 @@
1
+ require 'fonecal'
2
+
3
+ describe Fonecal::GrandPrix do
4
+ describe "Getting Belgian GP info" do
5
+ let(:belgianGP) { Fonecal::GrandPrix.new("http://www.formula1.com/races/in_detail/belgium_927/circuit_diagram.html") }
6
+
7
+ it "should have the correct race title" do
8
+ belgianGP.raceTitle.should eql "2014 Formula 1 Shell Belgian Grand Prix"
9
+ end
10
+
11
+ it "should get a country" do
12
+ belgianGP.country.should eql "Belgium"
13
+ end
14
+
15
+ it "has location object" do
16
+ belgianGP.location.should_not be_nil
17
+ end
18
+
19
+ describe "its events" do
20
+ describe "Practice 1" do
21
+ it "should have the right name" do
22
+ belgianGP.events.first.name.should eql "Practice 1"
23
+ end
24
+
25
+ it "should have the correct start time" do
26
+ belgianGP.events.first.start.should eql DateTime.new(2014, 8, 22, 10, 0, 0, '+1')
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
data/spec/race_spec.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'fonecal'
2
+
3
+ #describe Fonecal::Race do
4
+ #let(:event) { Fonecal::Race.new() }
5
+
6
+ #end
@@ -0,0 +1,17 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ RSpec.configure do |config|
8
+ config.treat_symbols_as_metadata_keys_with_true_values = true
9
+ config.run_all_when_everything_filtered = true
10
+ config.filter_run :focus
11
+
12
+ # Run specs in random order to surface order dependencies. If you find an
13
+ # order dependency and want to debug it, you can fix the order by providing
14
+ # the seed, which is printed after each run.
15
+ # --seed 1234
16
+ config.order = 'random'
17
+ end
data/spec/util_test.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'fonecal'
2
+
3
+ describe Fonecal::Util do
4
+ describe "#getRootURL" do
5
+ it "extracts formula1.com from any url" do
6
+ calSite = "http://www.formula1.com/races/calendar.html"
7
+ Fonecal::Util.getRootUrl(calSite).should eql('http://www.formula1.com')
8
+ end
9
+ end
10
+ end