Creating My Ultimate Packing List

I wonder if there is such a thing as over automating mundane tasks with a computer. Sometimes I feel that I might put more time into developing a computer based solution to a problem that far exceeds the amount of time it would take with a traditional method. Of course, this omits the benefits of practicing problem-solving and the possibility that it may ultimately take less time than if I would otherwise have to repeat the traditional method many times. Next week I am moving across the Atlantic and I don’t want to miss bringing anything important (I am notorious for forgetting small but important things at home). I decided to write a very simple script to produce a checklist that will keep my packing on track.

A couple of months ago, I discovered the Todo.txt system for managing my todo list. If you are not familiar with Todo.txt, it is a very simple method of keeping a todo list in… you guessed it… a text file! Obviously, this is nothing new or profound but the community at todotxt.com, specifically the originator Gina Trapani have defined a syntax for a todo list and have written a collection of applications to view and modify the todo file. When the todo.txt file is coupled with applications that can access Dropbox, you end up with a very simple system that can keep a todo list synchronized and usable on pretty much any platform that has Internet connectivity (in my case a laptop running Linux and my Android cellphone).

Below is a simple example of todo.txt syntax:

(A) Complete readings for economics lecture 5 @Home
(A) Submit course registration @School
(C) Organize shampoo in the shower @Home

The letter in parentheses represents the task priority from A-Z, the text is the task and the word after the ‘@’ represents a location or category for the task. For example, if you are at home, you can use the todo.txt apps (or just grep) any tasks for @Home.

So what does all this have to do with packing!? When I sat down to start my packing list for the move, I decided that I wanted to write it as a text file on Dropbox so that if I ever remembered something to add, I could add it right away instead of waiting until I got to the specific device that had the list. I decided against paper simply since it can get really messy if you keep brain dumping on the same sheet of paper… and if it gets lost… I just fell naturally into writing the packing list in something similar to todo.txt. In my case, I used the letter in parentheses to represent a category (‘C’ for clothes, ‘B’ for bike stuff, ‘T’ for toiletries, ‘E’ for electronics, etc). By keeping a category at the start of each row, I can easily sort my list by category or print out a mini-list for one category. I decided to use the ‘@’ notation to describe what bag or box the given item was being packed in, this way I could create a list to check each bag before the move.

Then I got a little carried away… I am an avid cyclist and will be bringing my bike with me in a box but I will also have several bike bags inside the box and each will have a list of bike-related items for example, I would have:

(B) Bike @BikeBox
(B) Pannier @BikeBox
(B) Tool Kit @Pannier
(E) Bike Computer @Pannier
(B) Bike Hand Pump @Pannier
(B) USB Front Bike Light @BikeBox

I decided that I wanted to print out the list, but I didn’t want to format it in a word processor in case I needed to add other items closer to packing day. This is when the computer geek in me got excited. It is no secret that I am a big fan of \LaTeX and I determined that the best way to get a good packing list would be to write a script to read in my todo.txt packing list and output a color-coded (by location) checklist as a pdf. The checklist would properly nest each of the packing locations and keep the categories separate within each location’s list. For example, in my list above, I would end up with a packing list for the bike box which would include the pannier and its sub-list, all sorted by category. Therefore I would end up with something like:

Bike Box
--- Bike (B)
--- Pannier (B)
--- --- Tool Kit (B)
--- --- Bike Hand Pump (B)
--- --- Bike Computer (E)
--- USB Front Bike Light (B)

The first step was to create a basic mock-up of the desired output in LaTeX:

\documentclass{letter}
\usepackage{color}
\usepackage{wasysym}

\let\olditem\item
\renewcommand\item{\olditem[$\Box$] }

\newenvironment{bagList}[2]
{
\par
$\Box$\textbf{#1}
\begin{itemize}
\color{#2}
}{ \end{itemize}}

\begin{document}

\begin{bagList}{Bike Box}{cyan}
\item Bike (B)
\begin{bagList}{Pannier (B)}{red}
\item Tool Kit (B)
\item Bike Hand Pump (B)
\item Bike Computer (E)
\end{bagList}
\item USB Front Bike Light
\end{bagList}

\end{document}

Giving the result:

testListPacking

The Code

*** Please go to the bottom of the page to find a collapsed frame with the full Python code.

I decided to write a Python script to read in my text based packing list and output the finished pdf. I selected Python since it is a simple object-oriented language that can be prototyped very quickly and can be called as a script. Python is also an excellent language for working with strings. The basic steps in the program are as follows:

  1. Read in the packing text file
  2. Create a list of items to pack
  3. Identify all of the unique packing locations ‘@’ symbols
  4. Create a tree of locations representing the idea that some bags might be stored in other bags or boxes
  5. Write a preamble with all the LaTeX declarations and environment definitions to a temporary file
  6. Write each item into the temporary file by travelling through the location tree
  7. Write a closing statement into the temporary file to end the document
  8. Call pdflatex to output the pdf
  9. Clean-up temporary files

LaTeX Constants

I set the following strings in the script that control the style of the LaTeX document. A new color is used for each location, therefore you need to have at least as many colors defined as locations. The preamble string has all of the LaTeX declarations and definitions that must be written before we start to add items. The post string simply contains the command to end the LaTeX document.

# Color list, need a different color for each location.  Add more if necessary.
latexColors = ['Bittersweet','Blue','Brown','CadetBlue','Cyan','DarkOrchid','ForestGreen','Goldenrod','JungleGreen','LimeGreen','Mahogany','Melon','Mulberry']

# The opening string for the LaTeX document, if you want to change the appearance of the lists look here
preamble = r'''\documentclass[12pt]{letter}
\usepackage[usenames,dvipsnames]{color}
\usepackage{wasysym}
\usepackage{enumitem}
\usepackage[margin=0.5in]{geometry}
\setlength\leftmargini {2em}
\setlength\leftmarginii {4em}
\setlength\leftmarginiii {6em}
\setlength\leftmarginiv {8em}
\let\olditem\item
\renewcommand\item{\olditem[$\Box$] }

% By using the itemize environment as a base, you are limited to 
% 4 levels of sub lists
\newenvironment{bagList}[2]
{
\par
%\vspace*{\baselineskip}
$\Box$  \textbf{#1}
\begin{itemize}[noitemsep, topsep=0pt]
\olditem[] % needed in case of immediately nested list
\vspace{-0.75\baselineskip}
\color{#2}
}{ \end{itemize}}

\begin{document}
'''

# The string to end the document
post = r'''\end{document}'''

Item Class

I wrote a simple class to represent each item that is in the list.
Each item has:

  1. A name
  2. A category
  3. A location

The __repr__ function allows me to change the way in which the item is printed in the LaTeX file, in this case I print the item name followed by its category.

# Define class for each item.  Each item has a category 
#(in brackets), a name and a location (after the @ sign)
class Item:
    def __init__(self, category, item, location):
        self.category = category
        self.item = item
        self.location = location

    def getCategory(self):
        return self.category

    def getItem(self):
        return self.item

    def getLocation(self):
        return self.location

    def __repr__(self):
        return "{0} {1}".format(self.item, self.category)

TreeNode Class

My main, and really only, challenge for this script was how to implement the tree of locations. It is necessary to determine the order of the locations before writing to the LaTeX file so as to ensure that nesting is properly managed. I am sure that there are many ways to implement this tree. The one I selected has the advantage of being simple and intuitive for this specific problem; however, if it was dealing with a very large tree, it would be very inefficient.

The algorithm I use for creating the tree is as follows:

  1. Generate a root node.
  2. Identify a set of the locations used in the list.
  3. If there are no locations in the set then end.
  4. For each location in the set:
    1. Search the list to see if this location is also an item, if so then set this location’s parent as the item’s ‘@’, else set the parent as root.
    2. Try to add the location to the tree (it will succeed if the location’s parent is already in the tree).
    3. If the insertion succeeded, remove this location from the set.
  5. Goto 3.

When you try to add an element to the tree, you specify its name and the name of its parent. The tree is traversed and if its parent is found then the element is added below it and the addition returns True. If the element cannot be added then the addition returns False. By taking the set of locations and repeatedly attempting to add each location you will eventually add all of the elements into the location tree.

# A very simple tree class.  This class automatically places 
# new nodes below the specified parent.  If the parent does 
# not exist, it does not enter the new node.
class TreeNode:
    def __init__(self, name):
        # desc is a dictionary of child nodes, the key is the 
        #  node name and the value is another TreeNode object
        self.desc = dict()
        self.name = name

    # Add a child node
    def addDesc(self, pt, parent):
        if parent == self.name:
            self.desc[pt.getName()] = pt
            return True
        else:
            for k in self.desc.keys():
                # Recursively search for the specified parent
                if(self.desc[k].addDesc(pt, parent)):
                    return True
            # Return false if the parent cannot be found
            return False

    def getName(self):
        return self.name

    # Returns a list of descendants, descendants with their own 
    # children are returned as lists within the list (therefore
    # this will return a tree using the called node as the root)
    def getDesc(self):
        listorder = []
        listorder.append(self.name)
        keylist = list(self.desc.keys())
        keylist.sort()
        for k in keylist:
            dlist = self.desc[k].getDesc()
            listorder.append(dlist)
        return listorder

Writing the Items

In order to write each item to the LaTeX file, I wrote a recursive function that writes all of the items for a given location in the tree. If while the function is writing a location and it discovers an item that is also a location, it will call the recursive function for that location in order to nest it into the list.

# A recursive function to write the latex representation 
# of the tree to a file t
# t --> file descriptor
# location --> treeNode to use as root
# itemList --> list of items to print
# colorIndex --> the current color to use
def latexWrite(t, location, itemList, colorIndex):
    # Start the list for location
    t.write("\\begin{{bagList}}{{{0}}}{{{1}}}\n".format(location[0], latexColors[colorIndex]))
    colorIndex = colorIndex+1
    # Go through each item in the itemList
    for i in itemList:
        found = False
        # Check to see if the current item belongs in the 
        # current location
        if i.getLocation() == location[0]:
            # Check to see if the current item is actually 
            # another location
            for l in location:
                if i.getItem() == l[0]:
                    # If the item is a location, then write 
                    # the list for that location
                    colorIndex = latexWrite(t, l, itemList, colorIndex)
                    found = True
            if found == False:
                # If the item is not a location then write it into
                # the list for the current location
                t.write("\\item {0}\n".format(i))
    t.write("\\end{bagList}\n")
    return colorIndex

Result

Once the code is called it uses subprocess to call pdflatex (this makes this script work only on Linux machines with pdflatex installed but I am sure you could set it up to call a pdf->latex program in Windows). The following is a capture of part of my actual packing list relating to the bike box:
finalList

This is the complete code for my packing list compiler:

#!/usr/bin/python

import sys
import re 
import subprocess
import os
import shlex

# Color list, need a different color for each location.  Add more if necessary.
latexColors = ['Bittersweet','Blue','Brown','CadetBlue','Cyan','DarkOrchid','ForestGreen','Goldenrod','JungleGreen','LimeGreen','Mahogany','Melon','Mulberry']

# The opening string for the LaTeX document, if you want to change the appearance of the lists look here
preamble = r'''\documentclass[12pt]{letter}
\usepackage[usenames,dvipsnames]{color}
\usepackage{wasysym}
\usepackage{enumitem}
\usepackage[margin=0.5in]{geometry}
\setlength\leftmargini {2em}
\setlength\leftmarginii {4em}
\setlength\leftmarginiii {6em}
\setlength\leftmarginiv {8em}
\let\olditem\item
\renewcommand\item{\olditem[$\Box$] }

% By using the itemize environment as a base, you are limited to 4 levels of sub lists
\newenvironment{bagList}[2]
{
\par
%\vspace*{\baselineskip}
$\Box$  \textbf{#1}
\begin{itemize}[noitemsep, topsep=0pt]
\olditem[] % needed in case of immediately nested list
\vspace{-0.75\baselineskip}
\color{#2}
}{ \end{itemize}}

\begin{document}
'''

# The string to end the document
post = r'''\end{document}'''

# Define class for each item.  Each item has a category (in brackets), a name and a location (after the @ sign)
class Item:
    def __init__(self, category, item, location):
        self.category = category
        self.item = item
        self.location = location

    def getCategory(self):
        return self.category

    def getItem(self):
        return self.item

    def getLocation(self):
        return self.location

    def __repr__(self):
        return "{0} ({1})".format(self.item, self.category)

# A very simple tree class.  This class automatically places new nodes below the specified parent.  If the parent does not exist, it does not enter the new node.
class TreeNode:
    def __init__(self, name):
        # desc is a dictionary of child nodes, the key is the node name and the value is another TreeNode object
        self.desc = dict()
        self.name = name

    # Add a child node
    def addDesc(self, pt, parent):
        if parent == self.name:
            self.desc[pt.getName()] = pt
            return True
        else:
            for k in self.desc.keys():
                # Recursively search for the specified parent
                if(self.desc[k].addDesc(pt, parent)):
                    return True
            # Return false if the parent cannot be found
            return False

    def getName(self):
        return self.name

    # Returns a list of descendants, descendants with their own children are returned as lists within the list (therefore this will return a tree using the called node as the root)
    def getDesc(self):
        listorder = []
        listorder.append(self.name)
        keylist = list(self.desc.keys())
        keylist.sort()
        for k in keylist:
            dlist = self.desc[k].getDesc()
            listorder.append(dlist)
        return listorder

# A recursive function to write the latex representation of the tree to a file t
# Parameters: t --> file descriptor, location --> treeNode to use as root, itemList --> list of items to print, colorIndex --> the current color to use
def latexWrite(t, location, itemList, colorIndex):
    # Start the list for location
    t.write("\\begin{{bagList}}{{{0}}}{{{1}}}\n".format(location[0], latexColors[colorIndex]))
    colorIndex = colorIndex+1
    # Go through each item in the itemList
    for i in itemList:
        found = False
        # Check to see if the current item belongs in the current location
        if i.getLocation() == location[0]:
            # Check to see if the current item is actually another location
            for l in location:
                if i.getItem() == l[0]:
                    # If the item is a location, then write the list for that location
                    colorIndex = latexWrite(t, l, itemList, colorIndex)
                    found = True
            if found == False:
                # If the item is not a location then write it into the list for the current location
                t.write("\\item {0}\n".format(i))
    t.write("\\end{bagList}\n")
    return colorIndex
            
def main(argv):
    # Open file passed through args
    if len(argv) < 2:
        print("Too few arguments, need a filename")
        sys.exit(1)

    with open(argv[1], 'r') as l:
        itemList = []
        # Go through list and identify all of the storage containers @'s, each item can be represented by a category, a name and a storage container
        for line in l:
            # create an item
            if re.search('@',line):
                try:
                    itemList.append(Item(re.search('\((.+?)\)', line).group(1), re.search('\) (.+?) @',line).group(1), re.search('@(.+?)$',line).group(1)))
                except AttributeError:
                    print("Problem with line: {0}".format(line))
            else:
                try:
                    itemList.append(Item(re.search('\((.+?)\)', line).group(1), re.search('\) (.+?)$',line).group(1),''))
                except AttributeError:
                    print("Problem with line: {0}".format(line))
            

        # Create list of storage locations
        locations = []
        for item in itemList:
            if item.getLocation() != '':
                locations.append(item.getLocation())
        locations = list(set(locations))
        # Make sure there are enough colors
        if len(locations) > len(latexColors):
            print("Too many locations for colors, add more colors to the list")
            sys.exit(1)

    
        # Sort all items by category then by name
        itemList.sort(key = lambda item: (item.location, item.category, item.item))

        # Create the ordered list of locations in tree form
        locations.sort()
        tree = TreeNode ("root")
        # Iterate through all locations repeatedly until all locations are in the tree
        while(len(locations)!=0):
            locations_orig = list(locations)
            for location in locations_orig:
                # Does location have a parent
                parent = "root"
                for item in itemList:
                    if item.getItem() == location:
                        parent = item.getLocation()
                if tree.addDesc(TreeNode(location),parent):
                    locations.remove(location)

        # Get the tree into a list
        orderedList = tree.getDesc()
        # Put each item into the appropriate list and create the latex file
        with open('list.tex','w') as t:
            t.write(preamble)
            colorIndex = 0
            for item in orderedList:
                if isinstance(item, list):
                    colorIndex = latexWrite(t,item,itemList,colorIndex)
            # Write any items without a location
            for item in itemList:
                if item.getLocation() == '':
                    t.write("\par$\Box$ {0}".format(item))
            t.write(post)
        proc=subprocess.Popen(shlex.split('pdflatex list.tex'))
        proc.communicate()
        os.unlink('list.tex')
        os.unlink('list.log')
        sys.exit(0)

if __name__ == "__main__":
    main(sys.argv)

Application to Todo Lists

After writing this script, I realized that this could be used to create formatted copies of a todo.txt list. I do not make extensive use of the ‘@’ notation in my todo lists but if you did and were interested in creating a nested list, you could make sub-locations into items… unfortunately this would mean that you would have todos that aren’t exactly todos. For example:

(A) Pick up keys for new office @AdminBuilding
(A) Make dinner to freeze for Tuesday @Home
(B) Check tracking number on package
(A) AdminBuilding @University
(B) Meet with HR rep @AdminBuilding
(C) Get parking pass @ParkingCentre
(A) ParkingCentre @University
(D) Get to know the campus @University

This results in:
todo

Now I actually need to pack :P

Please feel free to leave any comments you have. I appreciate constructive feedback on my code.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>