diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..163eb75 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0bbd4a9 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..765f0e9 --- /dev/null +++ b/.travis.yml @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8e60724 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 Anton Maminov + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e6f65c7 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# Geo::Coord + +Geo Coordinates class for Crystal, inspired by the Ruby's [geo_coord](https://github.com/zverok/geo_coord) gem. + +## Installation + +1. Add the dependency to your `shard.yml`: + + ```yaml + dependencies: + geo_coord: + github: mamantoha/geo_coord + ``` + +2. Run `shards install` + +## Usage + +```crystal +require "geo_coord" + +c = Geo::Coord.new(50.004444, 36.231389) + +c.to_s +# => "50°0'16\"N 36°13'53\"E" + +c.to_s(dms: false) +# => "50.004444,36.231389" + +c.strfcoord(%{%latd %latm' %lats" %lath, %lngd %lngm' %lngs" %lngh}) +# => "50°0'16\"N 36°13'53\"E" +``` + +## Contributing + +1. Fork it () +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 + +- [Anton Maminov](https://github.com/mamantoha) - creator and maintainer diff --git a/shard.yml b/shard.yml new file mode 100644 index 0000000..470bb0e --- /dev/null +++ b/shard.yml @@ -0,0 +1,9 @@ +name: geo_coord +version: 0.1.0 + +authors: + - Anton Maminov + +crystal: 0.31.1 + +license: MIT diff --git a/spec/geo_coord_spec.cr b/spec/geo_coord_spec.cr new file mode 100644 index 0000000..2c9ba40 --- /dev/null +++ b/spec/geo_coord_spec.cr @@ -0,0 +1,75 @@ +require "./spec_helper" + +describe Geo::Coord do + context "initialize" do + it "is initialized by (lat, lng)" do + c = Geo::Coord.new(50.004444, 36.231389) + + c.lat.should eq(50.004444) + c.lng.should eq(36.231389) + end + end + + context "strfcoord" do + it "renders components" do + pos = Geo::Coord.new(50.004444, 36.231389) + neg = Geo::Coord.new(-50.004444, -36.231389) + + pos.strfcoord("%latd").should eq("50") + neg.strfcoord("%latd").should eq("50") + neg.strfcoord("%latds").should eq("-50") + + pos.strfcoord("%latm").should eq("0") + pos.strfcoord("%lats").should eq("16") + pos.strfcoord("%lath").should eq("N") + neg.strfcoord("%lath").should eq("S") + + pos.strfcoord("%lat").should eq("%f" % pos.lat) + neg.strfcoord("%lat").should eq("%f" % neg.lat) + + pos.strfcoord("%lngd").should eq("36") + neg.strfcoord("%lngd").should eq("36") + neg.strfcoord("%lngds").should eq("-36") + + pos.strfcoord("%lngm").should eq("13") + pos.strfcoord("%lngs").should eq("53") + pos.strfcoord("%lngh").should eq("E") + neg.strfcoord("%lngh").should eq("W") + + pos.strfcoord("%lng").should eq("%f" % pos.lng) + neg.strfcoord("%lng").should eq("%f" % neg.lng) + end + + it "just leaves unknown parts" do + pos = Geo::Coord.new(50.004444, 36.231389) + pos.strfcoord("%latd %foo").should eq("50 %foo") + end + + it "understands everyting at once" do + pos = Geo::Coord.new(50.004444, 36.231389) + + pos.strfcoord(%{%latd %latm' %lats" %lath, %lngd %lngm' %lngs" %lngh}) + .should eq(%{50 0' 16" N, 36 13' 53" E}) + end + + it "can carry" do + pos = Geo::Coord.new(0.033333, 91.333333) + + pos.strfcoord("%latd %latm %lats, %lngd %lngm %lngs").should eq("0 2 0, 91 20 0") + pos.strfcoord("%latd %latm %0.2lats, %lngd %lngm %0.2lngs").should eq("0 2 0.00, 91 20 0.00") + pos.strfcoord("%latd %latm %0.3lats, %lngd %lngm %0.3lngs").should eq("0 1 59.999, 91 19 59.999") + end + end + + context "to_s" do + it "is convertible to string" do + c = Geo::Coord.new(50.004444, 36.231389) + c.to_s.should eq(%{50°0'16"N 36°13'53"E}) + c.to_s(dms: false).should eq("50.004444,36.231389") + + c = Geo::Coord.new(-50.004444, -36.231389) + c.to_s.should eq(%{50°0'16"S 36°13'53"W}) + c.to_s(dms: false).should eq("-50.004444,-36.231389") + end + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr new file mode 100644 index 0000000..53ed5fd --- /dev/null +++ b/spec/spec_helper.cr @@ -0,0 +1,2 @@ +require "spec" +require "../src/geo_coord" diff --git a/src/geo_coord.cr b/src/geo_coord.cr new file mode 100644 index 0000000..cec7fa1 --- /dev/null +++ b/src/geo_coord.cr @@ -0,0 +1,143 @@ +module Geo + class Coord + VERSION = "0.1.0" + + getter :lat + getter :lng + + INTFLAGS = /\+?/ + FLOATUFLAGS = /0\.\d+/ + FLOATFLAGS = /\+?#{FLOATUFLAGS}?/ + + DIRECTIVES = { + # Latitude + /%(#{FLOATUFLAGS})?lats/ => ->(m : Regex::MatchData) { "%#{m[1]? || "0."}f" }, + "%latm" => "%i", + /%(#{INTFLAGS})?latds/ => ->(m : Regex::MatchData) { "%#{m[1]}i" }, + "%latd" => "%i", + "%lath" => "%s", + /%(#{FLOATFLAGS})?lat/ => ->(m : Regex::MatchData) { "%#{m[1]}f" }, + # Longitude + /%(#{FLOATUFLAGS})?lngs/ => ->(m : Regex::MatchData) { "%#{m[1]? || "0."}f" }, + "%lngm" => "%i", + /%(#{INTFLAGS})?lngds/ => ->(m : Regex::MatchData) { "%#{m[1]}i" }, + "%lngd" => "%i", + "%lngh" => "%s", + /%(#{FLOATFLAGS})?lng/ => ->(m : Regex::MatchData) { "%#{m[1]}f" }, + } + + def initialize(@lat : Float32 | Float64, @lng : Float32 | Float64) + end + + # Returns latitude degrees + def latd : Int32 + lat.abs.to_i + end + + # Returns latitude minutes + def latm : Int32 + (lat.abs * 60).to_i % 60 + end + + # Returns latitude seconds + def lats : Float32 | Float64 + (lat.abs * 3600) % 60 + end + + # Returns latitude hemisphere + def lath : String + lat > 0 ? "N" : "S" + end + + # Returns longitude degrees + def lngd : Int32 + lng.abs.to_i + end + + # Returns longitude minutes + def lngm : Int32 + (lng.abs * 60).to_i % 60 + end + + # Returns longitude seconds + def lngs : Float32 | Float64 + (lng.abs * 3600) % 60 + end + + # Returns longitude hemisphere + def lngh : String + lng > 0 ? "E" : "W" + end + + def latds + lat.to_i + end + + def lngds + lng.to_i + end + + def strfcoord(formatstr) + h = full_hash + + DIRECTIVES.reduce(formatstr) do |memo, (from, to)| + memo.gsub(from) do + to = to.call($~) if to.is_a?(Proc) + + res = to % h + + if tmp = guard_seconds(to, res) + res, carrymin = tmp + + unless carrymin.empty? + if h[carrymin].is_a?(Int32) + tmp = h[carrymin].as(Int32) + h[carrymin] = tmp + 1 + end + end + end + + res + end + end + end + + # Returns a string representing coordinates. + # + # ``` + # g.to_s # => "50°0'16\"N 36°13'53\"E" + # g.to_s(dms: false) # => "50.004444,36.231389" + # ``` + def to_s(dms : Bool = true) + format = dms ? %{%latd°%latm'%lats"%lath %lngd°%lngm'%lngs"%lngh} : "%lat,%lng" + strfcoord(format) + end + + private def guard_seconds(pattern : String, result : String) : Array(String)? + if m = pattern.match(/<(lat|lng)s>/) + return [result, ""] unless m && result.starts_with?("60") + carry = "#{m[1]}m" + [pattern % {"lats" => 0, "lngs" => 0}, carry] + end + end + + private def full_hash : Hash(String, Int32 | Float32 | Float64 | String) + { + # Latitude + "latd" => latd, + "latds" => latds, + "latm" => latm, + "lats" => lats, + "lath" => lath, + "lat" => lat, + # Longitude + "lngd" => lngd, + "lngds" => lngds, + "lngm" => lngm, + "lngs" => lngs, + "lngh" => lngh, + "lng" => lng, + } + end + end +end