#!/usr/bin/env perl # # dstar-csv # John Simpson KG4ZOW 2024-08-25 # Last udpated 2025-08-31 # # Convert a CSV downloaded from http://dstarinfo.com/RepeaterDownloads.aspx # to use the groups I normally use. # ############################################################################### # # MIT License # # Copyright (C) 2024-2025 John Simpson # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the “Software”), # to deal in the Software without restriction, including without limitation # the rights to use, copy, modify, merge, publish, distribute, sublicense, # and/or sell copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. # ############################################################################### require 5.005 ; use strict ; use warnings ; use Getopt::Long qw ( :config require_order bundling ) ; ######################################## # List of columns to be included in output file my @columns = ( 'Group No' , 'Group Name' , 'Name' , 'Sub Name' , 'Repeater Call Sign' , 'Gateway Call Sign' , 'Frequency' , 'Dup' , 'Offset' , 'Mode' , 'TONE' , 'Repeater Tone' , 'RPT1USE' , 'Position' , 'Latitude' , 'Longitude' , 'UTC Offset' , ) ; ######################################## # Global data my %ohn = () ; # column name => number (output order) my @nh = () ; # column number => name (most recent header) my %hn = () ; # column name => number (most recent header) my %data = () ; # key => { column => value } my $filter_file = '' ; # filter file, if specified my %want_state = () ; # state => 1 (read from filter file) ############################################################################### sub usage(;$) { my $msg = ( shift || '' ) ; print < ) { ######################################## # Ignore comments and empty lines $line =~ s|#.*||s ; $line =~ s|\s+$|| ; next if ( $line eq '' ) ; ######################################## # Remember the line $want_state{$line} = 1 ; } close $fh ; } ############################################################################### # # Convert a state name my %states = () ; sub fix_state($) { my $x = shift ; ######################################## # If we've already "fixed" this state name, # return the result we came up with before if ( exists $states{$x} ) { return $states{$x} ; } ######################################## # Figure out the updated name my $rv = "US $x" ; if ( $x =~ m|^District Of Columbia$|i ) { $rv = 'US DC' ; } elsif ( $x =~ m|^North Carolina$|i ) { $rv = 'US N.Carolina' ; } elsif ( $x =~ m|^South Carolina$|i ) { $rv = 'US S.Carolina' ; } if ( length( $rv ) > 16 ) { print STDERR "WARNING: Group Name '$rv' is longer than 16 characters, truncating\n" ; $rv = substr( $rv , 0 , 16 ) ; } ######################################## # Remember it for next time debug "\%states{'$x'}='$rv'\n" ; $states{$x} = $rv ; return $rv ; } ############################################################################### ############################################################################### ############################################################################### GetOptions( 'h|help' => sub{ usage() } , 'D|debug' => \$do_debug , 'f|filter=s' => \$filter_file , ) || usage() ; ############################################################################### # # Build %ohn for my $n ( 0 .. $#columns ) { $ohn{$columns[$n]} = $n ; } ############################################################################### # # If a filter file was specified, read it if ( $filter_file ne '' ) { read_filter_file( $filter_file ) ; } ############################################################################### # # Process input lines while ( my $line = <> ) { $line =~ s|[\r\n]+$|| ; debug "$line\n" ; ############################################################ # If the line starts with a letter, it's a header. Remember which # headers are in which columns. if ( $line =~ m|^[a-z]|i ) { debug "line is header\n" ; @nh = split( /,/ , $line ) ; # column number => name %hn = () ; # column name => number for my $n ( 0 .. $#nh ) { unless ( exists $ohn{$nh[$n]} ) { print "WARNING: column name '$nh[$n]' not recognized\n" ; } $hn{$nh[$n]} = $n ; } next ; } ############################################################ # If the line starts with a digit, it's a repeater entry. if ( $line =~ m|^\d| ) { debug "line is data\n" ; ######################################## # Trim any extra spaces $line =~ s|\s*,\s*|,|g ; my @w = split( /,/ , $line ) ; ######################################## # Only include repeaters we're interested in # ### elsif ( ( $w[$hn{'Group Name'}] =~ m|USA| ) ### && ( exists $states{$w[$hn{'Sub Name'}]} ) ) my $ok = 0 ; if ( $w[$hn{'Group Name'}] eq 'Home' ) { $ok = 1 ; } elsif ( $w[$hn{'Group Name'}] =~ m|USA| ) { if ( $filter_file eq '' ) { $ok = 1 ; } else { my $st = $w[$hn{'Sub Name'}] ; if ( exists( $want_state{$st} ) ) { $ok = 1 ; } } } next unless( $ok ) ; ######################################## # Build the sort key # - report any duplicates in the input data my $k = $w[$hn{'Sub Name'}] . ',' . $w[$hn{'Name'}] . ',' . $w[$hn{'Repeater Call Sign'}] . ',' . $w[$hn{'Frequency'}] ; if ( exists $data{$k} ) { print STDERR "skipping duplicate key '$k'\n" ; next ; } ######################################## # Remember the fields my %r = () ; for my $c ( @columns ) { $r{$c} = $w[$hn{$c}] ; } ######################################## # Update the fields # - Fix state names which end up producing 'Group Name' values which # are longer than the radio's 16-character limit # - Input files have the region as 'Group Name', and the state name # as 'Sub Name'. I want "US (state)" as the 'Group Name', and have # no need for a 'Sub Name'. if ( $w[$hn{'Group Name'}] =~ m|USA| ) { $r{'Group Name'} = fix_state( $r{'Sub Name'} ) ; $r{'Sub Name'} = '' ; } ######################################## # Remeber the (possibly modified) output data debug "remembering data{'$k'}\n" ; $data{$k} = \%r ; next ; } ######################################## # If we get to this point, the input contains garbage print STDERR "ignoring unknown line\n$line\n\n" ; } ############################################################################### # # Print the sorted output # - In the process, generate new group numbers for each group (state) print join( ',' , @columns ) , "\n" ; my $gno = 0 ; my $gna = '' ; for my $k ( sort { lc($a) cmp lc($b) } keys %data ) { debug "out key '$k'\n" ; my $f = $data{$k} ; ######################################## # Need to generate new group numbers in the output if ( $f->{'Group Name'} ne $gna ) { $gno ++ ; $gna = $f->{'Group Name'} ; } $f->{'Group No'} = $gno ; ######################################## # Print the line my $comma = '' ; for my $c ( @columns ) { print $comma , $f->{$c} ; $comma = ',' ; } print "\n" ; }