## Form Letters

### November 30, 2010

We will use the input combinators from two previous exercises to process the letters:

```(define (form-letters schema-file data-file)   (let* ((schema (read-file schema-file))          (proc (lambda (rec) (print-form-letter schema rec))))     (with-input-from-file data-file       (lambda () (for-each-port read-csv-record proc)))))```

Depending on the format of your data file, you might need one of the other readers. And, if you wish, you can select only the records you wish with the `filter-port` combinator. Here is the function that prints the form letter associated with a single record:

```(define (print-form-letter schema rec)   (let loop1 ((schema schema))     (cond ((null? schema) (newline))           ((char=? (car schema) #\$)             (let loop2 ((schema (cdr schema)) (n 0))               (cond ((and (pair? schema) (char=? (car schema) #\$))                       (display #\$) (loop1 (cdr schema)))                     ((or (null? schema)                          (not (char-numeric? (car schema))))                       (display (list-ref rec n))                       (loop1 schema))                     (else (loop2 (cdr schema)                                  (+ (* n 10)                                     (- (char->integer (car schema))                                     (char->integer #\0))))))))           (else (display (car schema))                 (loop1 (cdr schema))))))```

The function writes the schema character by character from beginning to end. If it encounters a dollar sign, the next character is examined. If it is also a dollar sign, a dollar sign is written and processing continues with the next character. Otherwise, digits are gathered, and converted to a number, and the numbered field is written; if no digits follow the dollar sign, field \$0 is written. Here’s a sample run:

```> (form-letters "schema" "data") Welcome back, Jane! We hope that you and all the members of the Public family are constantly reminding your neighbors there on Maple Street to shop with us. As usual, we will ship your order to     Ms. Jane Q. Public     600 Maple Street     Your Town, Iowa 12345```

``` ```

```Welcome back, John! We hope that you and all the members of the Smith family are constantly reminding your neighbors there on Main Street to shop with us. As usual, we will ship your order to     Dr. John Z. Smith     1234 Main Street     Anytown, Missouri 63011```

We used `read-file` from the Standard Prelude and `read-csv-record` and `for-each-port` from the two previous exercises on text file databases. You can see the entire program assembled at http://programmingpraxis.codepad.org/IU214JGT.

Pages: 1 2

### 12 Responses to “Form Letters”

1. […] Praxis – Form Letters By Remco Niemeijer In today’s Programming Praxis exercise, we have to write a program to generate form letters. Let’s get […]

```import Control.Applicative ((*>), (<*>), (<\$>))
import Text.CSV
import Text.Parsec

fillWith :: String -> [String] -> String
fillWith text vars = either show concat \$ parse form "" text where
form = many \$ escape <|> count 1 anyChar
escape = char '\$' *> (string "\$" <|>
((vars !!) . read <\$> option "0" (many1 digit)))

formLetters :: FilePath -> FilePath -> IO [String]
formLetters schema vars = either (return . show) . map . fillWith <\$>
```
3. Chris Teixeira said

My (slightly golfed up) Ruby version:

d.each {|r| puts s.scan(/\\$\d+/).inject(s) {|a,m| a = a.gsub(m,r[m[/\d+/].to_i])}}

4. slabounty said

A ruby version …

```require 'csv'

CSV.foreach(ARGV) do |row|
form_filled = form
row.each_with_index do |v, i|
form_filled = form_filled.gsub("\$#{i}", v)
end
puts "#{form_filled}"
end
```

If you’re using ruby 1.8, then require fastercsv.

5. slabounty said

OK, both mine and Chris’ have the same two issues in that they don’t work with > 10 elements and the \$\$ doesn’t work. This one should work (it’s uglier, but works as is often the case). It works backwards down the list so that \$10 will be subbed out before \$1 and it will leave a “\$\$” alone. At the end it changes the “\$\$” to a single “\$”.

```require 'csv'

CSV.foreach(ARGV) do |row|
form_filled = form
(row.size-1).downto(0) do |i|
form_filled = form_filled.gsub(/([^\$])\\$#{i}/, "#{\$1}#{row[i]}")
end
form_filled.gsub!(/\\$\\$/, "\$")
puts "#{form_filled}"
end

```
6. ```<?php
function formLetters(\$schema_file,\$csv_inputfile) {
\$s = file_get_contents(\$schema_file);
\$dcount = 0;
if ((\$handle = fopen(\$csv_inputfile, "r")) !== FALSE) {
while ((\$data = fgetcsv(\$handle, 1000, ",")) !== FALSE) {
if (\$dcount == 0) \$dcount = count(\$data);
echo preg_replace(array('/\\$(\d+)/e','/\\$\\$/'), array('\$data[\\1]','\$'), \$s) . "\n";
}
}
}
formLetters('/dev/form1.schema','/dev/data.csv');
?>
```
7. Left the \$dcount variable in mine by accident, that would clean it up by two lines.

8. Axio said

Works with “\$\$” and more than 10 fields.

#!/usr/bin/env perl
(\$a,\$b)=@ARGV;open(\$S,\$a);\$s=join/\n/,<\$S>;open
(\$D,\$b);while(chomp(\$_=<\$D>)){@d=split/,/;\$_=\$s
;s/\\$(\d+)/\$d[\$1]/g;s/\\$\\$/\\$/g;print}

9. Graham said

A bit longer than everyone else’s. My answer can deal with arbitrarily many elements, but handles only the subcase
of the \$\$ problem where no other \$n remain after a \$\$ in a line of the schema.
I’ve included the imports, hashbang line, and the test at the end (copy-pasted schema and data from first page):

```#!/usr/bin/env python2.6

import csv
from string import digits

def form_letter(schema, data):
s = open(schema)
s.close()
output = ''
for line in lines:
x = 0
while x != -1:
x = line.find("\$", x)
i = x+1
while line[i] in digits:
i += 1
if i != x+1:    # swap out \$n with data's row[n]
n = int(line[x+1:i])
line = line[:x] + row[n] + line[i:]
else:
break   # handles \$\$ case, as long as no other numbers are
# after it
output = output + line
print output + "\n"
return

if __name__ == "__main__":
form_letter("schema.txt", "data.txt")
```
10. programmingpraxis said

Graham: Read the input a character at a time instead of a line at a time, and you won’t have a problem with \$n following \$\$ on the same schema line.

11. I came up with the following version, which can handle many elements but still fails when the schema contains “invalid” markup such as \$\$\$\$1 where the amount of \$s is unbalanced.
I also used some map/lambda foo to make it more interesting :)

```#!/usr/bin/env python2.6
import csv, re

def form_letter(schema, data):
v_re = re.compile(r'(?<!\$)\\$\d+')
order = map(lambda key: int(key[1:]), v_re.findall(tmpl))
tmpl = v_re.sub('%s', tmpl).replace('\$\$', '\$')

print tmpl % tuple(map(lambda key: row[key], order))

if __name__ == '__main__':
form_letter('schema.txt', 'data.csv')
```
12. John Doig said

import string, csv

def form_letter(letter, data):