cabrillo 0.0.1b
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/lib/cabrillo.rb +248 -0
- data/lib/contest_validators.rb +131 -0
- 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: []
|