Initial commit

This commit is contained in:
Nemo 2020-05-22 19:56:49 +05:30
commit 8531377d9c
9 changed files with 312 additions and 0 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*.cr]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
/docs/
/lib/
/bin/
/.shards/
*.dwarf
# Libraries don't need dependency lock
# Dependencies will be locked in applications that use them
/shard.lock

6
.travis.yml Normal file
View File

@ -0,0 +1,6 @@
language: crystal
# Uncomment the following if you'd like Travis to run specs and check code formatting
# script:
# - crystal spec
# - crystal tool format --check

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2020 Nemo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

44
README.md Normal file
View File

@ -0,0 +1,44 @@
# suntime
Crystal library for calculating sunrise and sunset times. Uses the algorithm from <http://www.edwilliams.org/sunrise_sunset_algorithm.htm>.
## Installation
1. Add the dependency to your `shard.yml`:
```yaml
dependencies:
suntime:
github: captn3m0/suntime
```
2. Run `shards install`
## Usage
```crystal
require "suntime"
# Time is optional, local time is used otherwise
Suntime.new(lat,long, time)
```
## Development
TODO: Write development instructions here
## Contributing
1. Fork it (<https://github.com/captn3m0/suntime/fork>)
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request
## Contributors
- [Nemo](https://github.com/captn3m0) - creator and maintainer
## License
Licensed under the [MIT License](https://nemo.mit-license.org/). See LICENSE file for details.

9
shard.yml Normal file
View File

@ -0,0 +1,9 @@
name: suntime
version: 0.1.0
authors:
- Nemo <me@captnemo.in>
crystal: 0.34.0
license: MIT

3
spec/spec_helper.cr Normal file
View File

@ -0,0 +1,3 @@
require "spec"
require "time"
require "../src/suntime"

56
spec/suntime_spec.cr Normal file
View File

@ -0,0 +1,56 @@
require "./spec_helper"
describe Suntime do
it "should return correct timestamp for example" do
lat = 40.9
lng = -74.3
l = Time::Location.load("America/New_York")
t = Suntime::Suntime.new(lat, lng, Time.local(1990, 6, 25, 0, 0, 0, location: l))
t.day_of_year.should eq(176)
t.approx_time.should eq(176.45638888888888)
t.approx_time(false).should eq(176.95638888888888)
t.sun_mean_anomaly(176.456).should eq(170.6260336)
t.sun_true_longitude(170.6260336).should eq(93.56567911851744)
t.sun_right_ascension(93.56567911851744).should eq(6.2589844084367705)
t.sun_local_hour_angle(93.56567911851744).should eq(-0.39570523787054585)
t.calculate_h(-0.39570523787054585).should eq(16.44600232000648)
t.local_mean_time(16.44600232000648, 6.2589844084367705, 176.456).should eq(5.441396301776585)
a = t.sunrise
a.year.should eq(1990)
a.month.should eq(6)
a.day.should eq(25)
a.hour.should eq(5)
a.minute.should eq(26)
a.second.should eq(29)
a.offset.should eq(-4 * 60 * 60)
a.location.should eq l
end
it "should work for bangalore" do
# +5.5
bangalore_tz = Time::Location.load("Asia/Kolkata")
t = Suntime::Suntime.new(12.955800, 77.620979, Time.local(2020, 5, 23, 0, 0, 0, location: bangalore_tz))
a = t.sunrise
a.year.should eq(2020)
a.month.should eq(5)
a.day.should eq(23)
a.hour.should eq(5)
a.minute.should eq(52)
a.second.should eq(41)
a.offset.should eq(5.5 * 60 * 60)
a.location.should eq bangalore_tz
b = t.sunset
b.year.should eq(2020)
b.month.should eq(5)
b.day.should eq(23)
b.hour.should eq(18)
b.minute.should eq(40)
b.second.should eq(0)
b.offset.should eq(5.5 * 60 * 60)
b.location.should eq bangalore_tz
end
end

155
src/suntime.cr Normal file
View File

@ -0,0 +1,155 @@
# TODO: Write documentation for `Suntime`
module Suntime
VERSION = "0.1.0"
# Currently based on http://www.edwilliams.org/sunrise_sunset_algorithm.htm
# TODO: Switch to https://www.esrl.noaa.gov/gmd/grad/solcalc/solareqns.PDF
# to use radians directly
class Suntime
@lng : Float64
@lat : Float64
@t : Time
def initialize(lat : Float64, lng : Float64, t : Time | Nil = nil)
@t = Time.local
@t = t unless t.nil?
@lng = lng
@lat = lat
end
def calculate_sun_time(sunrise = true)
t = approx_time(sunrise)
m = sun_mean_anomaly(t)
l = sun_true_longitude(m)
ra = sun_right_ascension(l)
cosH = sun_local_hour_angle(l)
h = calculate_h(cosH, sunrise)
fractional_time_to_proper_time local_mean_time(h, ra, t)
end
def fractional_time_to_proper_time(t_in_fractional_hours)
minutes = t_in_fractional_hours * 60
seconds = (minutes * 60).to_i
new_time = @t.at_beginning_of_day
new_time.shift(seconds: seconds)
end
def sunrise
calculate_sun_time(true)
end
def sunset
calculate_sun_time(false)
end
# 1. first calculate the day of the year
def day_of_year
n1 = (275 * @t.month / 9).floor
n2 = ((@t.month + 9) / 12).floor
n3 = (1 + ((@t.year - 4 * (@t.year / 4).floor + 2) / 3).floor)
return n1 - (n2 * n3) + @t.day - 30
end
# 2. convert the longitude to hour value and calculate an approximate time
# pass false for sunset
def approx_time(sunrise = true)
lngHour = @lng / 15
if sunrise
day_of_year + ((6 - lngHour) / 24)
else
day_of_year + ((18 - lngHour) / 24)
end
end
# 3. calculate the Sun's mean anomaly
def sun_mean_anomaly(t : Float64) : Float64
(0.9856 * t) - 3.289
end
def put_in_range(number, lower, upper, adjuster)
if number > upper
number -= adjuster
elsif number < lower
number += adjuster
else
number
end
end
# 4. calculate the Sun's true longitude
def sun_true_longitude(m)
m_in_radians = (Math::PI/180) * m
l = m + (1.916 * Math.sin(m_in_radians)) + (0.020 * Math.sin(2 * m_in_radians)) + 282.634
# NOTE: L potentially needs to be adjusted into the range [0,360) by adding/subtracting 360
put_in_range(l, 0, 360, 360)
end
# 5. calculate the Sun's right ascension
def sun_right_ascension(l)
l_in_radians = (Math::PI/180) * l
ra = (180/Math::PI) * Math.atan(0.91764 * Math.tan(l_in_radians))
# 5b. right ascension value needs to be in the same quadrant as L
l_quadrant = (l/90).floor * 90
ra_quadrant = (ra/90).floor * 90
ra = ra + (l_quadrant - ra_quadrant)
# NOTE: RA potentially needs to be adjusted into the range [0,360) by adding/subtracting 360
ra = put_in_range(ra, 0, 360, 360)
# 5c. right ascension value needs to be converted into hours
ra/15
end
# 6. calculate the Sun's declination
# 7a. calculate the Sun's local hour angle
def sun_local_hour_angle(l, zenith = :official)
case zenith
when :official
z = 1.58534074
when :civil
z = 1.67552
when :nautical
z = 1.78024
when :astronomical
z = 1.88496
else
z = 1.58534074
end
l_in_radians = (Math::PI/180) * l
sinDec = 0.39782 * Math.sin(l_in_radians)
cosDec = Math.cos(Math.asin(sinDec))
lat_in_radians = (Math::PI/180) * @lat
cosH = (Math.cos(z) - (sinDec * Math.sin(lat_in_radians))) / (cosDec * Math.cos(lat_in_radians))
raise "the sun never rises on this location (on the specified date)" if cosH > 1
raise "the sun never sets on this location (on the specified date)" if cosH < -1
cosH
end
# 7b. finish calculating H and convert into hours
def calculate_h(cosH, sunrise = true)
h = 0
if sunrise
h = 360 - (Math.acos(cosH) * (180/Math::PI))
else
h = Math.acos(cosH) * (180/Math::PI)
end
# convert it to hours
h/15
end
# 8. calculate local mean time of rising/setting
def local_mean_time(h, ra, t)
t = h + ra - (0.06571 * t) - 6.622
lngHour = @lng / 15
t = (t - lngHour)
t = put_in_range(t, 0, 24, 24)
t + (@t.offset / 3600)
end
end
end