├── .rspec
├── Gemfile
├── .gitignore
├── .travis.yml
├── lib
├── sun_times
│ └── version.rb
└── sun_times.rb
├── Rakefile
├── spec
├── spec_helper.rb
└── unit
│ └── ruby_sun_times_spec.rb
├── ruby-sun-times.gemspec
├── COPYING
└── README.md
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --format documentation
3 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /coverage/
2 | doc/*
3 | pkg/*
4 | Gemfile.lock
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | branches:
3 | only:
4 | - master
5 | rvm:
6 | - 1.8.7
7 | - 1.9.3
8 | - 2.0.0
9 | - 2.1.0
10 | script: "bundle exec rspec"
11 | cache: bundler
12 |
--------------------------------------------------------------------------------
/lib/sun_times/version.rb:
--------------------------------------------------------------------------------
1 | class SunTimes
2 | module VERSION #:nodoc:
3 | MAJOR = 0
4 | MINOR = 1
5 | TINY = 5
6 |
7 | STRING = [MAJOR, MINOR, TINY].join('.')
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'bundler/gem_tasks'
3 | require 'rspec/core/rake_task'
4 |
5 | task :default => :spec
6 |
7 | RSpec::Core::RakeTask.new do |t|
8 | t.pattern = 'spec/**/*_spec.rb'
9 | end
10 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | $LOAD_PATH << File.expand_path('../lib', File.dirname(__FILE__))
2 |
3 | if RUBY_VERSION >= '1.9'
4 | require 'simplecov'
5 | SimpleCov.start
6 | end
7 |
8 | require 'sun_times'
9 |
10 | RSpec.configure do |config|
11 | config.treat_symbols_as_metadata_keys_with_true_values = true
12 | config.run_all_when_everything_filtered = true
13 | config.filter_run :focus
14 | config.order = 'random'
15 | end
16 |
--------------------------------------------------------------------------------
/ruby-sun-times.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | $LOAD_PATH << File.expand_path('lib', File.dirname(__FILE__))
3 |
4 | require 'sun_times/version'
5 |
6 | Gem::Specification.new do |spec|
7 | spec.name = 'ruby-sun-times'
8 | spec.summary = 'Calculate sunrise and sunset times for a given time and place'
9 | spec.version = SunTimes::VERSION::STRING
10 | spec.licenses = ['MIT']
11 |
12 | spec.homepage = 'https://github.com/joeyates/ruby-sun-times'
13 | spec.author = 'Joe Yates'
14 | spec.email = 'joe.g.yates@gmail.com'
15 |
16 | spec.files = `git ls-files -z`.split("\0")
17 | spec.executables = spec.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18 | spec.test_files = spec.files.grep(%r{^spec/})
19 | spec.require_paths = ['lib']
20 |
21 | spec.add_development_dependency 'pry'
22 | spec.add_development_dependency 'rspec'
23 | if RUBY_VERSION >= '1.9'
24 | spec.add_development_dependency 'simplecov', '~> 0.7.1'
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/COPYING:
--------------------------------------------------------------------------------
1 | Copyright (c) 2010-2014 Joe Yates
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ruby-sun-times [][Continuous Integration]
2 |
3 | *Calculates sunrise and sunset times*
4 |
5 | * [Source Code]
6 | * [API documentation]
7 | * [Rubygem]
8 | * [Continuous Integration]
9 |
10 | [Source Code]: https://github.com/joeyates/ruby-sun-times "Source code at GitHub"
11 | [API documentation]: http://rubydoc.info/gems/ruby-sun-times/frames "RDoc API Documentation at Rubydoc.info"
12 | [Rubygem]: http://rubygems.org/gems/ruby-sun-times "Ruby gem at rubygems.org"
13 | [Continuous Integration]: http://travis-ci.org/joeyates/ruby-sun-times "Build status by Travis-CI"
14 |
15 | The algorithm comes from the [Almanac for computers][almanac-for-computers-1991].
16 |
17 | [almanac-for-computers-1991]: http://babel.hathitrust.org/cgi/pt?id=uc1.31822006852784;view=1up;seq=25
18 |
19 | # Usage
20 |
21 | ## Requiring
22 |
23 | In a Gemfile/Gemspec:
24 |
25 | ```ruby
26 | gem 'ruby-sun-times', require: 'sun_times'
27 | ```
28 |
29 | Directly:
30 |
31 | ```ruby
32 | require 'sun_times'
33 | ```
34 |
35 | ## Methods
36 |
37 | The two methods `rise` and `set` each return a Time.
38 |
39 | ```ruby
40 | day = Date.new(2010, 3, 8)
41 | latitude = 43.779
42 | longitude = 11.432
43 | sun_times = SunTimes.new
44 | sun_times.rise(day, latitude, longitude) # => 2010-03-08 05:39:53 UTC
45 | sun_times.set(day, latitude, longitude) # => 2010-03-08 17:11:16 UTC
46 | ```
47 |
48 | # References
49 |
50 | * http://www.astro.uu.nl/~strous/AA/en/reken/zonpositie.html - Calculations
51 | * http://williams.best.vwh.net/sunrise_sunset_algorithm.htm - Algorithm
52 | * http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/264573 - Ken Bloom's implementation
53 |
--------------------------------------------------------------------------------
/spec/unit/ruby_sun_times_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe SunTimes do
4 | let(:options) { {} }
5 | let(:day) { Date.new(2010, 3, 8) }
6 | let(:latitude) { 43.779 }
7 | let(:longitude) { 11.432 }
8 | let(:set) { Time.gm(2010, 3, 8, 17, 11, 16) }
9 | let(:rise) { Time.gm(2010, 3, 8, 5, 39, 53) }
10 | let(:midsummer) { Date.new(2010, 6, 21) }
11 | let(:midwinter) { Date.new(2010, 12, 21) }
12 | let(:north_cape_latitude) { 71.170219 }
13 | let(:north_cape_longitude) { 25.785556 }
14 |
15 | subject { described_class.new(options) }
16 |
17 | shared_examples_for 'never sets and never rises options' do |method|
18 | context ':never_rises_result' do
19 | let(:options) do
20 | {
21 | :never_rises_result => :never_rises,
22 | :never_sets_result => :never_sets,
23 | }
24 | end
25 |
26 | context 'never rises' do
27 | it 'uses the supplied value' do
28 | result = subject.send(method, midwinter, north_cape_latitude, north_cape_longitude)
29 | expect(result).to eq(:never_rises)
30 | end
31 | end
32 |
33 | context 'never sets' do
34 | it 'uses the supplied value' do
35 | result = subject.send(method, midsummer, north_cape_latitude, north_cape_longitude)
36 | expect(result).to eq(:never_sets)
37 | end
38 | end
39 | end
40 | end
41 |
42 | shared_examples_for 'timezones' do |method|
43 | context 'with timezone' do
44 | let(:zone) { Rational(-8, 24) }
45 | let(:day_with_zone) { DateTime.new(2011, 12, 13, 0, 0, 0, zone) }
46 |
47 | it 'calculates according to the supplied value' do
48 | set = subject.send(method, day_with_zone, 45.52, -122.681944)
49 | result_datetime = DateTime.new(set.year, set.month, set.day, set.hour, set.min, set.sec, 0)
50 |
51 | expect(result_datetime).to be > day_with_zone
52 | expect(result_datetime).to be <= day_with_zone + 1
53 | end
54 | end
55 | end
56 |
57 | shared_examples_for 'no event' do |method|
58 | context 'midnight sun' do
59 | it 'is nil' do
60 | result = subject.send(method, midsummer, north_cape_latitude, north_cape_longitude)
61 | expect(result).to be_nil
62 | end
63 | end
64 |
65 | context 'continuous night' do
66 | it 'is nil' do
67 | result = subject.send(method, midwinter, north_cape_latitude, north_cape_longitude)
68 | expect(result).to be_nil
69 | end
70 | end
71 | end
72 |
73 | shared_examples_for 'limiting cases' do |method|
74 | context 'last day of the year' do
75 | let(:zone) { Rational(-8, 24) }
76 | let(:day) { DateTime.new(2013, 12, 31, 8, 59, 5, zone) }
77 |
78 | it 'calculates correctly' do
79 | subject.send(method, day, 47.5, -122)
80 | end
81 | end
82 | end
83 |
84 | # SunTimes.rise is deprecated, use Suntimes.new.set
85 | describe '.rise' do
86 | it 'returns the sunrise time' do
87 | result = described_class.rise(day, latitude, longitude)
88 | expect(result).to be_within(1).of(rise)
89 | end
90 | end
91 |
92 | # SunTimes.set is deprecated, use Suntimes.new.set
93 | describe '.set' do
94 | it 'returns the sunset time' do
95 | result = described_class.set(day, latitude, longitude)
96 | expect(result).to be_within(1).of(set)
97 | end
98 | end
99 |
100 | describe '#rise' do
101 | it 'returns the sunrise time' do
102 | result = subject.rise(day, latitude, longitude)
103 | expect(result).to be_within(1).of(rise)
104 | end
105 |
106 | include_examples 'timezones', :rise
107 | include_examples 'limiting cases', :rise
108 | include_examples 'no event', :rise
109 |
110 | context 'options' do
111 | include_examples 'never sets and never rises options', :rise
112 | end
113 | end
114 |
115 | describe '#set' do
116 | it 'returns the sunset time' do
117 | result = subject.set(day, latitude, longitude)
118 | expect(result).to be_within(1).of(set)
119 | end
120 |
121 | include_examples 'timezones', :set
122 | include_examples 'limiting cases', :set
123 | include_examples 'no event', :rise
124 |
125 | context 'options' do
126 | include_examples 'never sets and never rises options', :set
127 | end
128 | end
129 | end
130 |
--------------------------------------------------------------------------------
/lib/sun_times.rb:
--------------------------------------------------------------------------------
1 | # Algorithm from http://williams.best.vwh.net/sunrise_sunset_algorithm.htm
2 |
3 | require 'date'
4 |
5 | class SunTimes
6 | DEFAULT_ZENITH = 90.83333
7 | KNOWN_EVENTS = [:rise, :set]
8 | DEGREES_PER_HOUR = 360.0 / 24.0
9 |
10 | attr_reader :options
11 |
12 | # Deprecated: use SunTimes.new.rise(...)
13 | def self.rise(date, latitude, longitude, options = {})
14 | new(options).rise(date, latitude, longitude)
15 | end
16 |
17 | # Deprecated: use SunTimes.new.set(...)
18 | def self.set(date, latitude, longitude, options = {})
19 | new(options).set(date, latitude, longitude)
20 | end
21 |
22 | # Deprecated: use SunTimes.new.rise/set(...)
23 | def self.calculate(event, date, latitude, longitude, options = {})
24 | new(options).calculate(event, date, latitude, longitude)
25 | end
26 |
27 | # * +options+ -
28 | # * :never_rises_result - the value to be returned if the sun never rises on the supplied date,
29 | # * :never_sets_result - the value to be returned if the sun never sets on the supplied date,
30 | # * :zenith - default 90.83333
31 | def initialize(options = {})
32 | @options = {
33 | :never_sets_result => nil,
34 | :never_rises_result => nil,
35 | :zenith => DEFAULT_ZENITH,
36 | }.merge(options)
37 | end
38 |
39 | # Calculates the sunrise time for a specific date and location
40 | #
41 | # ==== Parameters
42 | # * +date+ - An object that responds to :to_datetime.
43 | # * +latitude+ - The latitude of the location in degrees.
44 | # * +longitude+ - The longitude of the location in degrees.
45 | #
46 | # ==== Example
47 | # SunTimes.new.rise(Date.new(2010, 3, 8), 43.779, 11.432)
48 | # > Mon Mar 08 05:39:53 UTC 2010
49 | def rise(date, latitude, longitude)
50 | calculate(:rise, date, latitude, longitude)
51 | end
52 |
53 | # calculates sunset, see #rise for parameters
54 | def set(date, latitude, longitude)
55 | calculate(:set, date, latitude, longitude)
56 | end
57 |
58 | private
59 |
60 | def calculate(event, date, latitude, longitude)
61 | datetime = to_datetime(date)
62 | raise "Unknown event '#{event}'" unless KNOWN_EVENTS.include?(event)
63 |
64 | # lngHour
65 | longitude_hour = longitude / DEGREES_PER_HOUR
66 |
67 | # t
68 | base_time =
69 | if event == :rise
70 | 6.0
71 | else
72 | 18.0
73 | end
74 | approximate_time = datetime.yday + (base_time - longitude_hour) / 24.0
75 |
76 | # M
77 | mean_sun_anomaly = (0.9856 * approximate_time) - 3.289
78 |
79 | # L
80 | sun_true_longitude = mean_sun_anomaly +
81 | (1.916 * Math.sin(degrees_to_radians(mean_sun_anomaly))) +
82 | (0.020 * Math.sin(2 * degrees_to_radians(mean_sun_anomaly))) +
83 | 282.634
84 | sun_true_longitude = coerce_degrees(sun_true_longitude)
85 |
86 | # RA
87 | tan_right_ascension = 0.91764 * Math.tan(degrees_to_radians(sun_true_longitude))
88 | sun_right_ascension = radians_to_degrees(Math.atan(tan_right_ascension))
89 | sun_right_ascension = coerce_degrees(sun_right_ascension)
90 |
91 | # right ascension value needs to be in the same quadrant as L
92 | sun_true_longitude_quadrant = (sun_true_longitude / 90.0).floor * 90.0
93 | sun_right_ascension_quadrant = (sun_right_ascension / 90.0).floor * 90.0
94 | sun_right_ascension += (sun_true_longitude_quadrant - sun_right_ascension_quadrant)
95 |
96 | # RA = RA / 15
97 | sun_right_ascension_hours = sun_right_ascension / DEGREES_PER_HOUR
98 |
99 | sin_declination = 0.39782 * Math.sin(degrees_to_radians(sun_true_longitude))
100 | cos_declination = Math.cos(Math.asin(sin_declination))
101 |
102 | cos_local_hour_angle =
103 | (Math.cos(degrees_to_radians(options[:zenith])) - (sin_declination * Math.sin(degrees_to_radians(latitude)))) /
104 | (cos_declination * Math.cos(degrees_to_radians(latitude)))
105 |
106 | # the sun never rises on this location (on the specified date)
107 | return options[:never_rises_result] if cos_local_hour_angle > 1
108 | # the sun never sets on this location (on the specified date)
109 | return options[:never_sets_result] if cos_local_hour_angle < -1
110 |
111 | # H
112 | suns_local_hour =
113 | if event == :rise
114 | 360 - radians_to_degrees(Math.acos(cos_local_hour_angle))
115 | else
116 | radians_to_degrees(Math.acos(cos_local_hour_angle))
117 | end
118 |
119 | # H = H / 15
120 | suns_local_hour_hours = suns_local_hour / DEGREES_PER_HOUR
121 |
122 | # T = H + RA - (0.06571 * t) - 6.622
123 | local_mean_time = suns_local_hour_hours + sun_right_ascension_hours - (0.06571 * approximate_time) - 6.622
124 |
125 | # UT = T - lngHour
126 | gmt_hours = local_mean_time - longitude_hour
127 | gmt_hours -= 24.0 if gmt_hours > 24
128 | gmt_hours += 24.0 if gmt_hours < 0
129 |
130 | offset_hours = datetime.offset * 24.0
131 |
132 | if gmt_hours + offset_hours < 0
133 | next_day = next_day(datetime)
134 | return calculate(event, next_day.new_offset, latitude, longitude)
135 | end
136 | if gmt_hours + offset_hours > 24
137 | previous_day = prev_day(datetime)
138 | return calculate(event, previous_day.new_offset, latitude, longitude)
139 | end
140 |
141 | hour = gmt_hours.floor
142 | hour_remainder = (gmt_hours.to_f - hour.to_f) * 60.0
143 | minute = hour_remainder.floor
144 | seconds = (hour_remainder - minute) * 60.0
145 |
146 | Time.gm(datetime.year, datetime.month, datetime.day, hour, minute, seconds)
147 | end
148 |
149 | ############################
150 | # ruby 1.8 compatibility
151 |
152 | def to_datetime(date)
153 | if date.respond_to?(:to_datetime)
154 | date.to_datetime
155 | else
156 | values = [
157 | date.year,
158 | date.month,
159 | date.day,
160 | ]
161 | [:hour, :minute, :second, :zone].each do |m|
162 | values <<
163 | if date.respond_to?(m)
164 | date.send(m)
165 | else
166 | 0
167 | end
168 | end
169 | DateTime.new(*values)
170 | end
171 | end
172 |
173 | def prev_day(datetime)
174 | datetime - 1
175 | end
176 |
177 | def next_day(datetime)
178 | datetime + 1
179 | end
180 |
181 | ############################
182 |
183 | def degrees_to_radians(d)
184 | d.to_f / 360.0 * 2.0 * Math::PI
185 | end
186 |
187 | def radians_to_degrees(r)
188 | r.to_f * 360.0 / (2.0 * Math::PI)
189 | end
190 |
191 | def coerce_degrees(d)
192 | if d < 0
193 | d += 360
194 | return coerce_degrees(d)
195 | end
196 | if d >= 360
197 | d -= 360
198 | return coerce_degrees(d)
199 | end
200 | d
201 | end
202 | end
203 |
--------------------------------------------------------------------------------