pak_cities 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +55 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +228 -0
- data/Rakefile +12 -0
- data/build_output.txt +19 -0
- data/lib/pak_cities/city.rb +45 -0
- data/lib/pak_cities/configuration.rb +40 -0
- data/lib/pak_cities/data.json +2810 -0
- data/lib/pak_cities/distance_calculator.rb +39 -0
- data/lib/pak_cities/errors.rb +8 -0
- data/lib/pak_cities/query.rb +135 -0
- data/lib/pak_cities/statistics.rb +41 -0
- data/lib/pak_cities/validators.rb +26 -0
- data/lib/pak_cities/version.rb +5 -0
- data/lib/pak_cities.rb +39 -0
- data/sig/pak_cities.rbs +4 -0
- metadata +61 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PakCities
|
|
4
|
+
module DistanceCalculator
|
|
5
|
+
EARTH_RADIUS_KM = 6371.0
|
|
6
|
+
KM_TO_MILES = 0.621371
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def haversine_distance(lat1, lon1, lat2, lon2, unit: :km)
|
|
11
|
+
Validators.validate_coordinates!(lat1, lon1)
|
|
12
|
+
Validators.validate_coordinates!(lat2, lon2)
|
|
13
|
+
|
|
14
|
+
rad_per_deg = Math::PI / 180.0
|
|
15
|
+
|
|
16
|
+
dlat = (lat2 - lat1) * rad_per_deg
|
|
17
|
+
dlon = (lon2 - lon1) * rad_per_deg
|
|
18
|
+
|
|
19
|
+
a = Math.sin(dlat / 2)**2 +
|
|
20
|
+
Math.cos(lat1 * rad_per_deg) * Math.cos(lat2 * rad_per_deg) *
|
|
21
|
+
Math.sin(dlon / 2)**2
|
|
22
|
+
c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
|
23
|
+
|
|
24
|
+
distance_km = EARTH_RADIUS_KM * c
|
|
25
|
+
convert_distance(distance_km, unit)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def convert_distance(distance_km, unit)
|
|
29
|
+
case unit
|
|
30
|
+
when :km
|
|
31
|
+
distance_km
|
|
32
|
+
when :miles
|
|
33
|
+
distance_km * KM_TO_MILES
|
|
34
|
+
else
|
|
35
|
+
raise ArgumentError, "Invalid unit: #{unit}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PakCities
|
|
4
|
+
module Query
|
|
5
|
+
def all
|
|
6
|
+
cities
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def count
|
|
10
|
+
cities.size
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def find(name)
|
|
14
|
+
cities.find { |city| match_name?(city.name, name) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def find!(name)
|
|
18
|
+
find(name) || raise(CityNotFoundError, "City not found: #{name}")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def search(query)
|
|
22
|
+
query = normalize_string(query)
|
|
23
|
+
cities.select { |city| normalize_string(city.name).include?(query) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def where(conditions = {})
|
|
27
|
+
result = cities
|
|
28
|
+
|
|
29
|
+
if conditions[:province]
|
|
30
|
+
province = conditions[:province]
|
|
31
|
+
result = result.select { |city| match_name?(city.province, province) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
result = result.select { |city| city.population >= conditions[:min_population] } if conditions[:min_population]
|
|
35
|
+
|
|
36
|
+
result = result.select { |city| city.population <= conditions[:max_population] } if conditions[:max_population]
|
|
37
|
+
|
|
38
|
+
result
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def by_province(province)
|
|
42
|
+
cities.select { |city| match_name?(city.province, province) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def top_by_population(limit = 10)
|
|
46
|
+
Validators.validate_positive_integer!(limit, "limit")
|
|
47
|
+
cities.sort_by(&:population).reverse.take(limit)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def by_name(order: :asc)
|
|
51
|
+
sorted = cities.sort_by(&:name)
|
|
52
|
+
order == :desc ? sorted.reverse : sorted
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def by_population(order: :desc)
|
|
56
|
+
sorted = cities.sort_by(&:population)
|
|
57
|
+
order == :desc ? sorted.reverse : sorted
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def random(limit = 1)
|
|
61
|
+
Validators.validate_positive_integer!(limit, "limit")
|
|
62
|
+
cities.sample(limit)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def provinces
|
|
66
|
+
cities.map(&:province).uniq.sort
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def grouped_by_province
|
|
70
|
+
cities.group_by(&:province)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def nearest_to(lat, lng, limit = 5)
|
|
74
|
+
Validators.validate_coordinates!(lat, lng)
|
|
75
|
+
Validators.validate_positive_integer!(limit, "limit")
|
|
76
|
+
|
|
77
|
+
unit = configuration.distance_unit
|
|
78
|
+
|
|
79
|
+
cities_with_distance = cities.map do |city|
|
|
80
|
+
distance = DistanceCalculator.haversine_distance(
|
|
81
|
+
lat, lng, city.latitude, city.longitude, unit: unit
|
|
82
|
+
)
|
|
83
|
+
[city, distance]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
cities_with_distance.sort_by { |_, distance| distance }.take(limit).map(&:first)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def within_bounds(min_lat:, max_lat:, min_lng:, max_lng:)
|
|
90
|
+
Validators.validate_coordinates!(min_lat, min_lng)
|
|
91
|
+
Validators.validate_coordinates!(max_lat, max_lng)
|
|
92
|
+
|
|
93
|
+
cities.select do |city|
|
|
94
|
+
city.latitude.between?(min_lat, max_lat) &&
|
|
95
|
+
city.longitude.between?(min_lng, max_lng)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def distance_between(city1_name, city2_name)
|
|
100
|
+
city1 = find!(city1_name)
|
|
101
|
+
city2 = find!(city2_name)
|
|
102
|
+
|
|
103
|
+
DistanceCalculator.haversine_distance(
|
|
104
|
+
city1.latitude, city1.longitude,
|
|
105
|
+
city2.latitude, city2.longitude,
|
|
106
|
+
unit: configuration.distance_unit
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def same_province?(city1_name, city2_name)
|
|
111
|
+
city1 = find!(city1_name)
|
|
112
|
+
city2 = find!(city2_name)
|
|
113
|
+
city1.province == city2.province
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def reload!
|
|
117
|
+
@cities = nil
|
|
118
|
+
cities
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def match_name?(str1, str2)
|
|
124
|
+
if configuration.case_sensitive
|
|
125
|
+
str1 == str2
|
|
126
|
+
else
|
|
127
|
+
str1.downcase == str2.downcase
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def normalize_string(str)
|
|
132
|
+
configuration.case_sensitive ? str : str.downcase
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PakCities
|
|
4
|
+
module Statistics
|
|
5
|
+
def total_population
|
|
6
|
+
cities.sum(&:population)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def average_population
|
|
10
|
+
return 0 if cities.empty?
|
|
11
|
+
|
|
12
|
+
total_population / cities.size.to_f
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def median_population
|
|
16
|
+
return 0 if cities.empty?
|
|
17
|
+
|
|
18
|
+
sorted = cities.map(&:population).sort
|
|
19
|
+
mid = sorted.size / 2
|
|
20
|
+
sorted.size.odd? ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2.0
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def population_by_province
|
|
24
|
+
grouped_by_province.transform_values do |cities_in_province|
|
|
25
|
+
cities_in_province.sum(&:population)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def cities_count_by_province
|
|
30
|
+
grouped_by_province.transform_values(&:size)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def largest_city
|
|
34
|
+
cities.max_by(&:population)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def smallest_city
|
|
38
|
+
cities.min_by(&:population)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PakCities
|
|
4
|
+
module Validators
|
|
5
|
+
LATITUDE_RANGE = (-90..90).freeze
|
|
6
|
+
LONGITUDE_RANGE = (-180..180).freeze
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def validate_coordinates!(latitude, longitude)
|
|
11
|
+
unless LATITUDE_RANGE.cover?(latitude)
|
|
12
|
+
raise InvalidCoordinatesError, "Latitude must be between -90 and 90, got #{latitude}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
return if LONGITUDE_RANGE.cover?(longitude)
|
|
16
|
+
|
|
17
|
+
raise InvalidCoordinatesError, "Longitude must be between -180 and 180, got #{longitude}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def validate_positive_integer!(value, name)
|
|
21
|
+
return if value.is_a?(Integer) && value.positive?
|
|
22
|
+
|
|
23
|
+
raise ArgumentError, "#{name} must be a positive integer, got #{value.inspect}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/pak_cities.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "pak_cities/version"
|
|
4
|
+
require_relative "pak_cities/errors"
|
|
5
|
+
require_relative "pak_cities/configuration"
|
|
6
|
+
require_relative "pak_cities/validators"
|
|
7
|
+
require_relative "pak_cities/city"
|
|
8
|
+
require_relative "pak_cities/distance_calculator"
|
|
9
|
+
require_relative "pak_cities/query"
|
|
10
|
+
require_relative "pak_cities/statistics"
|
|
11
|
+
require "json"
|
|
12
|
+
|
|
13
|
+
module PakCities
|
|
14
|
+
DATA_FILE = File.join(__dir__, "pak_cities", "data.json")
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
include Query
|
|
18
|
+
include Statistics
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def cities
|
|
23
|
+
@cities ||= load_cities
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def load_cities
|
|
27
|
+
data = JSON.parse(File.read(DATA_FILE))
|
|
28
|
+
data.map do |city_data|
|
|
29
|
+
City.new(
|
|
30
|
+
name: city_data["city"],
|
|
31
|
+
province: city_data["province"],
|
|
32
|
+
latitude: city_data["latitude"],
|
|
33
|
+
longitude: city_data["longitude"],
|
|
34
|
+
population: city_data["pop2025"]
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/sig/pak_cities.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: pak_cities
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Talha Razzaq
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Provides easy access to Pakistan cities data with powerful query methods.
|
|
13
|
+
email:
|
|
14
|
+
- talha36201@gmail.com
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- CHANGELOG.md
|
|
20
|
+
- CODE_OF_CONDUCT.md
|
|
21
|
+
- LICENSE.txt
|
|
22
|
+
- README.md
|
|
23
|
+
- Rakefile
|
|
24
|
+
- build_output.txt
|
|
25
|
+
- lib/pak_cities.rb
|
|
26
|
+
- lib/pak_cities/city.rb
|
|
27
|
+
- lib/pak_cities/configuration.rb
|
|
28
|
+
- lib/pak_cities/data.json
|
|
29
|
+
- lib/pak_cities/distance_calculator.rb
|
|
30
|
+
- lib/pak_cities/errors.rb
|
|
31
|
+
- lib/pak_cities/query.rb
|
|
32
|
+
- lib/pak_cities/statistics.rb
|
|
33
|
+
- lib/pak_cities/validators.rb
|
|
34
|
+
- lib/pak_cities/version.rb
|
|
35
|
+
- sig/pak_cities.rbs
|
|
36
|
+
homepage: https://github.com/Talha380/pak_cities
|
|
37
|
+
licenses:
|
|
38
|
+
- MIT
|
|
39
|
+
metadata:
|
|
40
|
+
allowed_push_host: https://rubygems.org
|
|
41
|
+
homepage_uri: https://github.com/Talha380/pak_cities
|
|
42
|
+
source_code_uri: https://github.com/Talha380/pak_cities
|
|
43
|
+
changelog_uri: https://github.com/Talha380/pak_cities/blob/master/CHANGELOG.md
|
|
44
|
+
rdoc_options: []
|
|
45
|
+
require_paths:
|
|
46
|
+
- lib
|
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
48
|
+
requirements:
|
|
49
|
+
- - ">="
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: 2.7.0
|
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
53
|
+
requirements:
|
|
54
|
+
- - ">="
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: '0'
|
|
57
|
+
requirements: []
|
|
58
|
+
rubygems_version: 4.0.3
|
|
59
|
+
specification_version: 4
|
|
60
|
+
summary: A Ruby gem for querying Pakistan cities data
|
|
61
|
+
test_files: []
|