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.
@@ -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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PakCities
4
+ class Error < StandardError; end
5
+ class CityNotFoundError < Error; end
6
+ class InvalidCoordinatesError < Error; end
7
+ class InvalidConfigurationError < Error; end
8
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PakCities
4
+ VERSION = "0.1.0"
5
+ 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
@@ -0,0 +1,4 @@
1
+ module PakCities
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
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: []