Perl: Text-based Address Books

Over the years, I've stored my addresses in a variety of formats. I used to have a Palm Pilot. These days, I have an Android phone, and Google keeps track of my addresses. However, I've always had a second copy of my addresses in a text file. The format looks something like this:
A & R Stock to Performance:
Address: 2849 Willow Pass Rd. #A, Concord, CA 94519
Work Phone: (925) 689-1846
There are many advantages to using a text-based format. For instance, since I'm an expert Vim users, I can search and edit the file extremely quickly. Best of all, the format works on every operating system and never goes obsolete. The only downside is that I have to input the address twice: once for Google and once for my own notes.

Long ago, I wrote a Perl script to convert my notes file into a Palm Pilot database. Even though I don't have a Palm Pilot anymore, I keep the script around. I can easily alter it to output the addresses in different formats.

If you like the format I used above, here's the source code so that you can hack it to do whatever you want:
#!/usr/bin/perl -w

#
# Author: Shannon -jj Behrens
# Email: jjinux@gmail.com
#
# This program converts the addresses stored in my notes file into a Palm
# database file (a pdb), which I can then import into my Handspring Visor.
# See usage for more details.
#
# Global variables:
# $_0 - This is $0, but without the path.
# $pdb:: - This is a handle to the Palm address database being edited.
#

# %phone_mappings - Mappings from things such as "Cell" to integers. These
# values were deduced from the @Palm::Address::phoneLabels array as well
# as usage in my notes.txt file.
%phone_mappings:: = (
Work => 0,
Home => 1,
Fax => 2,
Other => 3,
Email => 4,
Cell => 7
);

# $MAX_PHONES - The maximum amount of "Phone" type fields.
$MAX_PHONES:: = 8;

use strict;
use Palm::PDB;
use Palm::StdAppInfo;
use Palm::Address;


# Output usage information to the user and exit with value 64 (see man
# sysexits on a FreeBSD machine).
#
# By the way, here's how I use this program:
#
# mkdir palm
# pilot-xfer -b palm
# ./notes_to_palm ~/notes.txt palm/AddressDB.pdb
# pilot-xfer -r palm
# rm -r palm
sub usage {
print "usage: $_0:: notes.txt AddressDB.pdb\n";
exit 64;
}


# There's an ugly "bug" that causes Perl 5.6 to complain about unamed
# categories. Hence, I have to do a little bit of initializing or else I get a
# whole bunch of uninitialized warnings.
sub init_category_names {
for (my $i=0; $i < Palm::StdAppInfo::numCategories; $i++) {
if (!defined($pdb::->{appinfo}{categories}[$i]{name})) {
$pdb::->{appinfo}{categories}[$i]{name} = "";
}
}
$pdb::->{appinfo}{dirtyFields} = 1;
}


# Return a new record. I have to do a little bit of initializing or else I get
# a whole bunch of uninitialized warnings. I wonder why the libraries don't do
# this automatically since they initialize everything to undef anyway. Also,
# I'll insert a phoneIndex variable into the $record so that I can use the
# phone slots on a first come first serve basis.
sub new_record {
my $record = $pdb::->new_Record;
foreach my $field (qw( name firstName company phone1 phone2 phone3
phone4 phone5 phone6 phone7 phone8 address
city state zipCode country title custom1
custom2 custom3 custom4 note )) {
$record->{fields}{$field} = "";
}
$record->{phoneLabel}{reserved} = 0;
$record->{fields}{phoneIndex} = 1;
return $record;
}


# Process a name line, such as "Shannon -jj Behrens (author)".
sub handle_name {
my ($fullname) = @_;

# Check for note.
if ($fullname =~ /^(.*)\((.*)\)/) {
$fullname = $1;
$record::->{fields}{note} = $2;
}

# Assume last word is last name, everything else is first name.
my @name = split / /, $fullname;
$record::->{fields}{name} = pop @name;
$record::->{fields}{firstName} = join " ", @name;
}


# Process a phone type (e.g. Cell) and a value (e.g. "(925) 209-6439"). Please
# read p5-palm's Address.pm module to see the interesting way Palm phone
# numbers work. When possible, use the email address as the primary "Phone"
# field.
sub handle_phonelike_field {
my ($type, $value) = @_;

my $phone_index = $record::->{fields}{phoneIndex};
return if ($phone_index > $MAX_PHONES::);
my $phone_field = "phone$phone_index";
$record::->{fields}{$phone_field} = $value;
$record::->{phoneLabel}{$phone_field} = $phone_mappings::{$type};
$record::->{phoneLabel}{display} = $phone_index - 1
if ($type eq "Email");
$record::->{fields}{phoneIndex}++;
}


# Process an address line, such as "125 Gilger Ave., Martinez, CA 94553".
sub handle_address {
my ($fulladdress) = @_;

my @address = split /, /, $fulladdress;
$record::->{fields}{address} = $address[0];
$record::->{fields}{city} = $address[1];
my @state_zip = split / /, $address[2];
$record::->{fields}{state} = $state_zip[0];
$record::->{fields}{zipCode} = $state_zip[1];
}


# Handle one line, $_, from the notes.txt file. Remember, there must be at
# least one blank line after every address record in the notes.txt file in
# order to flush it to disk.
#
# Creates globals: $section::, $record::.
sub handle_line {
# Ignore anything not in the Contacts section.
if ($_ =~ /-{5,} ([^\-]+) -{5,}/) {
$section:: = $1;
return;
}
return if (!defined($section::) or
$section:: ne "Contacts");

# A blank line is a signal to flush the current record, if any. Otherwise,
# it can just be ignored.
if ($_ =~ /^[ \t]*$/) {
if (defined($record::)) {
$pdb::->append_Record($record::);
$record:: = undef;
}
return ;
}

# If the line doesn't start with a space, this is a new record's name.
# Start a new record, and then handle the name.
if ($_ =~ /^([^ ].*):/) {
my $fullname = $1;
$record:: = new_record;
handle_name $fullname;
return;
}

# Check for phone numbers and email addresses.
foreach my $type (keys %phone_mappings::) {
if ((($type eq "Email") and ($_ =~ / Email: (.*)/)) or
(($type ne "Email") and ($_ =~ / $type Phone: (.*)/))) {
handle_phonelike_field $type, $1;
return;
}
}

# Check for address.
if ($_ =~ / Address: (.*)/) {
handle_address $1;
return;
}

# Check for company.
if ($_ =~ / Company: (.*)/) {
$record::->{fields}{company} = $1;
return;
}

# Anything else can be appended to the additional notes section.
s/^\s+//;
$record::->{fields}{note} .= $_;
print STDERR "Additional notes: $_";
}


my @pieces = split /\//, $0;
$_0:: = $pieces[$#pieces];
usage if (scalar(@ARGV) != 2);
my $notes = shift;
my $addresses = shift;
open(NOTES, $notes) || die "$_0::: $notes: $!\n";
$pdb:: = new Palm::Address;
init_category_names;
handle_line while (<NOTES>);
$pdb::->Write($addresses);
close NOTES;

Comments