cabrillo 0.0.1b

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/lib/cabrillo.rb +248 -0
  3. data/lib/contest_validators.rb +131 -0
  4. metadata +45 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f9aa61856cc36ff7dff85e30c469c30619b59e7de1081f308a75b553f6cee8ca
4
+ data.tar.gz: 44ff1ba9a385c320f02384ddb209de73d0222c57d7df595bd9c0e7d2c045aaeb
5
+ SHA512:
6
+ metadata.gz: bed3037e4cb58779fe66c9548eacd4643bbb37fa0155c22590ca31dd94c47ce63ebb0f4e791ed578101678ec43b760113beae98bd6cf419f85067c2f83ea7ad7
7
+ data.tar.gz: 4397437c7b9ee26707742446e1adc683a41ce8f7f06e95ae17e3d47b1c4ed0fac2d6ae0cca613523c96fded8f55d2fbfbf1bc2852f64176ac8c1a12ae256d3f6
data/lib/cabrillo.rb ADDED
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env ruby
2
+ # Cabrillo - Amateur Radio Log Library
3
+ #
4
+ # This library handles the parsing and generation of the Cabrillo ham radio
5
+ # logging format, commonly used by the ARRL for contesting.
6
+ #
7
+ # Written by Ricky Elrod (github: @CodeBlock) and released an MIT license.
8
+ # https://www.github.com/CodeBlock/cabrillo-gem
9
+
10
+ $: << File.dirname(__FILE__)
11
+ require 'contest_validators'
12
+ require 'date'
13
+ require 'time'
14
+
15
+ # TODO: Split these into their own gem because they are handy. :-)
16
+ class String
17
+ def to_hz
18
+ freq_split = self.split('.')
19
+ hertz = freq_split[0].to_i * 1000000 # MHz
20
+
21
+ # Handle KHz
22
+ if not freq_split[1].nil?
23
+ freq_split[1] += '0' while freq_split[1].length < 3
24
+ hertz += freq_split[1].to_i * 1000 # KHz
25
+ end
26
+
27
+ # Handle Hz
28
+ if not freq_split[2].nil?
29
+ freq_split[2] += '0' while freq_split[2].length < 3
30
+ hertz += freq_split[2].to_i # Hz
31
+ end
32
+ hertz
33
+ end
34
+ end
35
+
36
+ class Integer
37
+ def to_mhz
38
+ self.to_s.reverse.gsub(/(.{3})(?=.)/, '\1.\2').reverse
39
+ end
40
+ end
41
+ # END TODO
42
+
43
+ class InvalidDataError < StandardError; end
44
+
45
+ class Cabrillo
46
+ @raise_on_invalid_data = true
47
+
48
+ CABRILLO_VERSION = '3.0' # The current version of the spec, our default.
49
+
50
+ # Public: Creates an instance of Cabrillo from a Hash of log data
51
+ #
52
+ # options - A Hash which contains data from a cabrillo log
53
+ #
54
+ # Returns an instance of Cabrillo.
55
+ def initialize(options = {})
56
+ # Let all the given entries automagically become instance variables.
57
+ options.each do |key, value|
58
+ instance_variable_set("@#{key}", value)
59
+ this = class << self; self; end
60
+ this.class_eval { attr_accessor key }
61
+ end
62
+
63
+ # Defaults and sanity checks can go here if they need to.
64
+ @version = options[:version] || CABRILLO_VERSION
65
+ end
66
+
67
+ # Public: Return the collected data as a Hash.
68
+ #
69
+ # Returns the data that was parsed (or given) as a Hash.
70
+ def to_hash
71
+ h = {}
72
+ self.instance_variables.each do |variable|
73
+ h[variable[1..-1].to_sym] = self.instance_variable_get(variable)
74
+ end
75
+ h
76
+ end
77
+
78
+ class << self
79
+ attr_accessor :raise_on_invalid_data
80
+
81
+ # Public: Parses a log (a string containing newlines) into a Cabrillo
82
+ # instance.
83
+ #
84
+ # log_contents - A String containing the entire to parse.
85
+ #
86
+ # TODO: Use a parsing lib like Treetop maybe?
87
+ #
88
+ # Returns an instance of Cabrillo.
89
+ def parse(log_contents)
90
+ cabrillo_info = Hash.new { |h,k| h[k] = [] }
91
+ log_contents.lines.each do |line|
92
+ line = line.strip
93
+
94
+ # Ignore comments. (See README.md for info.)
95
+ next if line.start_with? '#' or line.start_with? '//' or line.empty?
96
+
97
+ # Info that can only appear once.
98
+ cabrillo_info.merge! split_basic_line(line, 'START-OF-LOG', :version)
99
+ cabrillo_info.merge! split_basic_line(line, 'CALLSIGN', :callsign)
100
+ cabrillo_info.merge! split_basic_line(line, 'CATEGORY-ASSISTED', :category_assisted, ContestValidators::CATEGORY_ASSISTED)
101
+ cabrillo_info.merge! split_basic_line(line, 'CATEGORY-BAND', :category_band, ContestValidators::CATEGORY_BAND)
102
+ cabrillo_info.merge! split_basic_line(line, 'CATEGORY-MODE', :category_mode, ContestValidators::CATEGORY_MODE)
103
+ cabrillo_info.merge! split_basic_line(line, 'CATEGORY-OPERATOR', :category_operator, ContestValidators::CATEGORY_OPERATOR)
104
+ cabrillo_info.merge! split_basic_line(line, 'CATEGORY-POWER', :category_power, ContestValidators::CATEGORY_POWER)
105
+ cabrillo_info.merge! split_basic_line(line, 'CATEGORY-STATION', :category_station, ContestValidators::CATEGORY_STATION)
106
+ cabrillo_info.merge! split_basic_line(line, 'CATEGORY-TIME', :category_time, ContestValidators::CATEGORY_TIME)
107
+ cabrillo_info.merge! split_basic_line(line, 'CATEGORY-TRANSMITTER', :category_transmitter, ContestValidators::CATEGORY_TRANSMITTER)
108
+ cabrillo_info.merge! split_basic_line(line, 'CATEGORY-OVERLAY', :category_overlay, ContestValidators::CATEGORY_OVERLAY)
109
+ cabrillo_info.merge! split_basic_line(line, 'CLAIMED-SCORE', :claimed_score, ContestValidators::CLAIMED_SCORE)
110
+ cabrillo_info.merge! split_basic_line(line, 'CLUB', :club)
111
+ cabrillo_info.merge! split_basic_line(line, 'CONTEST', :contest, ContestValidators::CONTEST)
112
+ cabrillo_info.merge! split_basic_line(line, 'CREATED-BY', :created_by)
113
+ cabrillo_info.merge! split_basic_line(line, 'EMAIL', :email)
114
+ cabrillo_info.merge! split_basic_line(line, 'LOCATION', :location)
115
+ cabrillo_info.merge! split_basic_line(line, 'NAME', :name)
116
+ cabrillo_info.merge! split_basic_line(line, 'ADDRESS-CITY', :address_city)
117
+ cabrillo_info.merge! split_basic_line(line, 'ADDRESS-STATE-PROVINCE', :address_state_province)
118
+ cabrillo_info.merge! split_basic_line(line, 'ADDRESS-POSTALCODE', :address_postalcode)
119
+ cabrillo_info.merge! split_basic_line(line, 'ADDRESS-COUNTRY', :address_country)
120
+
121
+ # TODO: It would be great to remove some of the redundancy here.
122
+ address = split_basic_line(line, 'ADDRESS', :address)
123
+ cabrillo_info[:address] << address[:address] unless address.empty?
124
+
125
+ soapbox = split_basic_line(line, 'SOAPBOX', :soapbox)
126
+ cabrillo_info[:soapbox] << soapbox[:soapbox] unless soapbox.empty?
127
+
128
+ club = split_basic_line(line, 'CLUB', :club)
129
+ cabrillo_info[:club] << club[:club] unless club.empty?
130
+
131
+ operators = split_basic_line(line, 'OPERATORS', :operators)
132
+ cabrillo_info[:operators] << club[:operators] unless operators.empty?
133
+
134
+ # If we already parsed in a contest then we're good. If not, we don't
135
+ # know what to parse as, so skip.
136
+ if line.start_with? "QSO: " and cabrillo_info[:contest]
137
+ cabrillo_info[:qsos] << parse_qso(line, cabrillo_info[:contest])
138
+ end
139
+ end
140
+ Cabrillo.new(cabrillo_info)
141
+ end
142
+
143
+ # Public: A wrapper to Cabrillo.parse() to parse a log from a file.
144
+ #
145
+ # file_path - The path to the logfile to parse.
146
+ #
147
+ # Returns what Cabrillo.parse() returns, an instance of Cabrillo.
148
+ def parse_file(file_path)
149
+ Cabrillo.parse(IO.read(file_path))
150
+ end
151
+
152
+
153
+ private
154
+ # Private: Parses a specific line of the log, in most cases.
155
+ #
156
+ # line - The String of log line to parse.
157
+ # key - The key to look for in the line.
158
+ # hash_key - The key to use in the resulting Hash.
159
+ #
160
+ # Throws an Exception if validators are given but the data does not match
161
+ # one of them.
162
+ #
163
+ # Returns a Hash of {:hash_key => value_from_parsed_line} or nil if the key
164
+ # wasn't found.
165
+ def split_basic_line(line, key, hash_key, validators = [])
166
+ line_key, line_value = line.split(/:\s+/, 2)
167
+
168
+ if line_key == key
169
+ okay = true
170
+ unless validators.empty?
171
+ okay = false
172
+ validators.each do |v|
173
+ okay = true and break if v.class.to_s == 'Regexp' and line_value =~ v
174
+ okay = true and break if v.class.to_s == 'String' and line_value == v
175
+ end
176
+ end
177
+
178
+ if okay || !@raise_on_invalid_data
179
+ { hash_key => line_value.strip }
180
+ elsif !validators.empty? && @raise_on_invalid_data
181
+ raise InvalidDataError, "Invalid value given for key `#{line_key}`."
182
+ end
183
+ else
184
+ { }
185
+ end
186
+ end
187
+
188
+ # Private: Parses a QSO: line based on the contest type.
189
+ #
190
+ # qso_line - The Strnig containing the line of the logfile that we are
191
+ # parsing. Starts with "QSO:"
192
+ # contest - A String, the name of the contest that we are parsing.
193
+ #
194
+ # Returns a Hash containing the parsed result.
195
+ def parse_qso(qso_line, contest)
196
+ if @raise_on_invalid_data
197
+ raise InvalidDataError, "Invalid contest: #{contest}" unless ContestValidators::CONTEST.include? contest
198
+ raise InvalidDataError, "Line does not start with 'QSO: '" unless qso_line.start_with? "QSO: "
199
+ end
200
+ qso_line.gsub!(/^QSO: /, "")
201
+
202
+ # Basic structure
203
+ qso = {
204
+ :exchange => {
205
+ :sent => {},
206
+ :received => {}
207
+ }
208
+ }
209
+
210
+ # In any and all cases, the first fields are: frequency, mode, date, time.
211
+ # Store the exchange/everything else into an array (using splat) for
212
+ # later.
213
+ qso[:frequency], qso[:mode], date, time, *exchange = qso_line.split
214
+
215
+ # Parse the date and time into a Time object.
216
+ qso[:time] = Time.parse(DateTime.strptime("#{date} #{time}", '%Y-%m-%d %H%M').to_s)
217
+
218
+ # Transmitted callsign always comes first.
219
+ qso[:exchange][:sent][:callsign] = exchange.shift
220
+
221
+ # Parse the rest of the exchange
222
+ case contest
223
+ when 'CQ-160-CW', 'CQ-160-SSB', 'CQ-WPX-RTTY', 'CQ-WPX-CW', 'CQ-WPX-SSB', 'CQ-WW-RTTY', 'CQ-WW-CW', 'CQ-WW-SSB', 'ARRL-DX-CW', 'ARRL-DX-SSB', 'IARU-HF', 'ARRL-10', 'ARRL-160', 'JIDX-CW', 'JIDX-SSB', 'STEW-PERRY', 'OCEANIA-XD-CW', 'OCEANIA-DX-SSB', 'AP-SPRINT', 'NEQP', 'ARRL-FIELD-DAY'
224
+ qso[:exchange][:sent][:rst] = exchange.shift
225
+ qso[:exchange][:sent][:exchange] = exchange.shift
226
+
227
+ qso[:exchange][:received][:callsign] = exchange.shift
228
+ qso[:exchange][:received][:rst] = exchange.shift
229
+ qso[:exchange][:received][:exchange] = exchange.shift
230
+ qso[:exchange][:received][:transmitter_id] = exchange.shift
231
+ when 'ARRL-SS-CW', 'ARRL-SS-SSB'
232
+ qso[:exchange][:sent][:serial_number] = exchange.shift
233
+ qso[:exchange][:sent][:precedence] = exchange.shift
234
+ qso[:exchange][:sent][:check] = exchange.shift
235
+ qso[:exchange][:sent][:arrl_section] = exchange.shift
236
+
237
+ qso[:exchange][:received][:callsign] = exchange.shift
238
+ qso[:exchange][:received][:serial_number] = exchange.shift
239
+ qso[:exchange][:received][:precedence] = exchange.shift
240
+ qso[:exchange][:received][:check] = exchange.shift
241
+ qso[:exchange][:received][:arrl_section] = exchange.shift
242
+ end
243
+
244
+ qso
245
+ end
246
+
247
+ end
248
+ end
@@ -0,0 +1,131 @@
1
+ class ContestValidators
2
+ CATEGORY_ASSISTED = [
3
+ 'ASSISTED',
4
+ 'NON-ASSISTED',
5
+ ]
6
+
7
+ CATEGORY_BAND = [
8
+ 'ALL',
9
+ '160M',
10
+ '80M',
11
+ '40M',
12
+ '20M',
13
+ '15M',
14
+ '10M',
15
+ '6M',
16
+ '2M',
17
+ '222',
18
+ '432',
19
+ '902',
20
+ '1.2G',
21
+ '2.3G',
22
+ '3.4G',
23
+ '5.7G',
24
+ '10G',
25
+ '24G',
26
+ '47G',
27
+ '75G',
28
+ '119G',
29
+ '142G',
30
+ '241G',
31
+ 'Light',
32
+ ]
33
+
34
+ CATEGORY_MODE = [
35
+ 'SSB',
36
+ 'CW',
37
+ 'RTTY',
38
+ 'MIXED',
39
+ ]
40
+
41
+ CATEGORY_OPERATOR = [
42
+ 'SINGLE-OP',
43
+ 'MULTI-OP',
44
+ 'CHECKLOG',
45
+ ]
46
+
47
+ CATEGORY_POWER = [
48
+ 'HIGH',
49
+ 'LOW',
50
+ 'QRP',
51
+ ]
52
+
53
+ CATEGORY_STATION = [
54
+ 'FIXED',
55
+ 'MOBILE',
56
+ 'PORTABLE',
57
+ 'ROVER',
58
+ 'EXPEDITION',
59
+ 'HQ',
60
+ 'SCHOOL',
61
+ ]
62
+
63
+ CATEGORY_TIME = [
64
+ '6-HOURS',
65
+ '12-HOURS',
66
+ '24-HOURS',
67
+ ]
68
+
69
+ CATEGORY_TRANSMITTER = [
70
+ 'ONE',
71
+ 'TWO',
72
+ 'LIMITED',
73
+ 'UNLIMITED',
74
+ 'SWL',
75
+ ]
76
+
77
+ CATEGORY_OVERLAY = [
78
+ 'ROOKIE',
79
+ 'TB-WIRED',
80
+ 'NOVICE-TECH',
81
+ 'OVER-50',
82
+ ]
83
+
84
+ CLAIMED_SCORE = [
85
+ /\d+/,
86
+ ]
87
+
88
+ CONTEST = [
89
+ 'AP-SPRINT',
90
+ 'ARRL-10',
91
+ 'ARRL-160',
92
+ 'ARRL-DX-CW',
93
+ 'ARRL-DX-SSB',
94
+ 'ARRL-SS-CW',
95
+ 'ARRL-SS-SSB',
96
+ 'ARRL-UHF-AUG',
97
+ 'ARRL-VHF-JAN',
98
+ 'ARRL-VHF-JUN',
99
+ 'ARRL-VHF-SEP',
100
+ 'ARRL-RTTY',
101
+ 'BARTG-RTTY',
102
+ 'CQ-160-CW',
103
+ 'CQ-160-SSB',
104
+ 'CQ-WPX-CW',
105
+ 'CQ-WPX-RTTY',
106
+ 'CQ-WPX-SSB',
107
+ 'CQ-VHF',
108
+ 'CQ-WW-CW',
109
+ 'CQ-WW-RTTY',
110
+ 'CQ-WW-SSB',
111
+ 'DARC-WAEDC-CW',
112
+ 'DARC-WAEDC-RTTY',
113
+ 'DARC-WAEDC-SSB',
114
+ 'FCG-FQP',
115
+ 'IARU-HF',
116
+ 'JIDX-CW',
117
+ 'JIDX-SSB',
118
+ 'NA-SPRINT-CW',
119
+ 'NA-SPRINT-SSB',
120
+ 'NCCC-CQP',
121
+ 'NEQP',
122
+ 'OCEANIA-DX-CW',
123
+ 'OCEANIA-DX-SSB',
124
+ 'RDXC',
125
+ 'RSGB-IOTA',
126
+ 'SAC-CW',
127
+ 'SAC-SSB',
128
+ 'STEW-PERRY',
129
+ 'TARA-RTTY',
130
+ ]
131
+ end
metadata ADDED
@@ -0,0 +1,45 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cabrillo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1b
5
+ platform: ruby
6
+ authors:
7
+ - AmirolAhmad
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-07-17 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A ruby library to parse and generate ham radio Cabrillo log files.
14
+ email: dev.amirol@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/cabrillo.rb
20
+ - lib/contest_validators.rb
21
+ homepage: http://rubygems.org/gems/cabrillo
22
+ licenses:
23
+ - MIT
24
+ metadata: {}
25
+ post_install_message:
26
+ rdoc_options: []
27
+ require_paths:
28
+ - lib
29
+ required_ruby_version: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ required_rubygems_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">"
37
+ - !ruby/object:Gem::Version
38
+ version: 1.3.1
39
+ requirements: []
40
+ rubyforge_project:
41
+ rubygems_version: 2.7.6
42
+ signing_key:
43
+ specification_version: 4
44
+ summary: Cabrillo!
45
+ test_files: []