Scripting with SBCL

Tagged as LISP, Programming

Written on 2009-09-09 17:40:14

Over the labor day weekend, I had fun. I avoided dreary schoolwork, I played guitar, I hung out with cool people, I celebated my second anniversary with a lovely lady, I wrote code.

One bit of code I worked on is a script to read in AT&T call logs and figure out the 5 people you call most often. As you might guess, this can be useful for people contemplating switching to T-Mobile (say, for an upcoming piece of hardware designed for their 3G network). In short, the script is run from the command prompt with ./myfaves.lisp and prints out the 5 people you've talked to the most based on your call logs and the total percentage of your minutes those calls account for. The call logs this script processes can be downloaded from wireless.att.com. Login to your wireless.att.com account, go to "Bill & Payments". Under "Wireless Statement Summary" click the "Call Details" tab and finally scroll down a bit and click "Download Call Details". Using the dropdown box, select each month then click CSV format and submit. Put all those files in the same directory and then execute the following lisp script from that directory.


#!/usr/bin/sbcl --script
(declaim (sb-ext:muffle-conditions style-warning))

(eval-when (:compile-toplevel :load-toplevel :execute)
(let ((*standard-output* (make-broadcast-stream))
(*error-output* (make-broadcast-stream)))
(require 'asdf)
(require 'split-sequence)
(require 'osicat)
(require 'cl-containers)))

(defpackage :my-faves
(:use :common-lisp :cl-containers :osicat)
(:import-from split-sequence split-sequence))

(in-package :my-faves)

;; ATT CSV info
;; call logs start on line 24, entries on every other line (evens)
;; voice calls final line starts with "Totals"
;; 5th comma-entry is number, 7th is duration in minutes

(defparameter *months* nil)
(defparameter *results* (make-array 6 :fill-pointer 0))
(defparameter *call-log* (make-container 'sorted-list-container
:test #'equal
:key #'car
:sorter #'string<))

(defun init ()
(loop for path in (list-directory (truename ".")) do
(let* ((pathstr (native-namestring path))
(ext (subseq pathstr (- (length pathstr) 3))))
(when (string= "csv" ext)
(push path *months*)))))

(defun find-faves ()
(loop for file in *months* do
(load-calls file))
(analyze-data)
(print-results))

(defun load-calls (path)
(catch 'load-calls
(with-open-file (in path)
(loop for i from 1 to 23 do
(read-line in nil))
(loop for line = (read-line in nil) do
(parse-call line)))))

(defun parse-call (csv-line)
(cond ((string= "" csv-line))
((finished-voice csv-line) (throw 'load-calls 'done))
(t (let* ((split-line (split-sequence #\, csv-line))
(number (fifth split-line))
(minutes (parse-integer (seventh split-line))))
(insert-call-sorted number minutes)))))

(defun finished-voice (csv-line)
(string= "Totals" (subseq csv-line 0 6)))

(defun insert-call-sorted (number minutes)
(let ((present (search-for-item *call-log* number :key #'car)))
(if present
(incf (cdr (search-for-item *call-log* number :key #'car)) minutes)
(insert-item *call-log* (cons number minutes)))))

(defun analyze-data ()
(ensure-sorted *call-log*)
(sort-elements *call-log* #'> :key #'cdr)
(loop for number from 0 to 4 do
(vector-push (item-at *call-log* number) *results*))
(let ((total-free (loop for i from 0 to 4 summing (cdr (aref *results* i))))
(total-mins (reduce-elements *call-log* #'+ :key #'cdr)))
(setf (aref *results* 5) (round (* 100 (/ total-free total-mins))))))

(defun print-results ()
(format t "AT&T -> T-Mobile myFaves Recommendations:~%")
(format t "-----------------------------------------~%")
(format t "According to our analysis of your call logs, these are your 5 most frequently dialed numbers.~%")
(format t "-----------------------------------------~%~%")
(loop for i from 0 to 4 do
(format t "~A whom you spoke to for ~A minutes.~%~%" (car (aref *results* i)) (cdr (aref *results* i))))
(format t "These numbers should be your myFaves as they accounted for ~A% of your total minutes.~%" (aref *results* 5)))

(init)
(find-faves)


I'm sure it's not the prettiest, lispiest code out there but it could be an awful lot worse. Also, sbcl emitted style-warnings when the script was run. This behavior surprised me a little bit. After all, if I'm running the script I have little need for the style-warnings. After some digging, I learned enough to write this patch for the program which suppressed the undesirable output. I hope to submit a patch to the SBCL manual in the next week or so that notes this may be desired as nothing on the current page regarding sbcl --script would indicate that any output from the compiler would appear.
comments powered by Disqus

Unless otherwise credited all material Creative Commons License by Brit Butler