cabrillo 0.0.1b
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|