• Aucun résultat trouvé

Example: Date Arithmetic

Dans le document ANSI Common Lisp (Page 109-116)

Specialized Data Structures

Section 2.13 mentioned that the first argument to do had to be a list of specifications for variables, each possibly of the form

5.7 Example: Date Arithmetic

For more on errors and conditions, see Section 14.6 and Appendix A.

Sometimes you want code to be proof against interruptions like throws and errors. By using an unwind-protect, you can ensure that such interrup-tions won'tleave your program in an inconsistent state. An unwind-protect takes any number of arguments and returns the value of the first. However, the remaining expressions will be evaluated even if the evaluation of the first is interrupted.

> ( s e t f x 1) 1

> ( c a t c h ' a b o r t (unwind-protect

(throw 'abort 99) (setf x 2))) 99

> x 2

Here, even though the throw sends control back to the waiting catch, unwind-protect ensures that the second expression gets evaluated on the way out. Whenever certain actions have to be followed by some kind of cleanup or reset, unwind-protect may be useful. One example is men-tioned on page 121.

5.7 Example: Date Arithmetic

In some applications it's useful to be able to add and subtract dates—to be able to calculate, for example, that the date 60 days after December 17,1997

5.7 EXAMPLE: DATE ARITHMETIC 93 is February 15,1998. In this section we will write a utility for date arithmetic.

We will convert dates to integers, with zero fixed at January 1, 2000. We will be able to manipulate such integers using the built-in + and - functions, and when we're finished, convert the result back to a date.

To convert a date to an integer, we will add together the number of days represented by each of its components. For example, the integer value of November 13, 2004 is the sum of the number of days up to 2004, plus the number of days up to November, plus 13.

One thing we'll need here is a table listing the number of days up to the start of each month in a non-leap year. We can use Lisp to derive the contents of this table. We start by making a list of the lengths of each of the months:

> (setf mon '(31 28 31 30 31 30 31 31 30 31 30 31)) (31 28 31 30 31 30 31 31 30 31 30 31)

We can test that the lengths add up properly by applying + to the list:

> (apply #'+ mon) 365

Now if we reverse the list and use m a p l i s t to apply + to successive cdrs, we can get the number of days up to the beginning of each month:

> (setf nom (reverse mon))

(31 30 31 30 31 31 30 31 30 31 28 31)

> (setf sums (maplist #'(lambda (x) (apply #'+ x)) nom))

(365 334 304 273 243 212 181 151 120 90 59 31)

> (reverse sums)

(31 59 90 120 151 181 212 243 273 304 334 365)

These numbers indicate that there are 31 days up to the start of February, 59 up to the start of March, and so on.

The list we just created is transformed into a vector in Figure 5.1, which contains the code for converting dates to integers.

There are four stages in the life of a typical Lisp program: it is written, then read, then compiled, then run. One of the distinctive things about Lisp is that it's there at every stage. You can invoke Lisp when your program is running, of course, but you can also invoke it when your program is compiled (Section 10.2) and when it is read (Section 14.3). The way we derived month shows how you can use Lisp even as you're writing a program.

Efficiency usually only matters in the last of the four stages, run-time.

In the first three stages you can feel free to take advantage of the power and flexibility of lists without worrying about the cost.

(defconstant month

#(0 31 59 90 120 151 181 212 243 273 304 334 365)) (defconstant yzero 2000)

(defun leap? (y)

(and (zerop (mod y 4)) (or (zerop (mod y 400))

(not (zerop (mod y 100)))))) (defun date->num (d m y)

(+ (- d 1) (month-num m y) (year-num y))) (defun month-num (m y)

(+ (svref month ( - m l ) )

(if (and (> m 2) (leap? y)) 1 0))) (defun year-num (y)

(let ((d 0)) (if (>= y yzero)

(dotimes (i (- y yzero) d)

(incf d (year-days (+ yzero i)))) (dotimes (i (- yzero y) (- d))

(incf d (year-days (+ y i))))))) (defun year-days (y) (if (leap? y) 366 365))

Figure 5.1: Date arithmetic: Converting dates to integers.

If you used the code in Figure 5.1 to drive a time machine, people would probably disagree with you about the date when you arrived. European dates have shifted, even in comparatively recent times, as people got a more precise idea of the length of a year. In English-speaking countries, the last such discontinuity was in 1752, when the date went straight from September 2 to September 14.°

The number of days in a year depends on whether it is a leap year. A year is a leap year if it is divisible by 4, unless it is divisible by 100, in which case it isn't—unless it is divisible by 400, in which case it is. So 1904 was a leap year, 1900 wasn't, and 1600 was.

To determine whether one number is divisible by another we use the function mod, which returns the remainder after division:

SUMMARY 95

> (mod 23 5) 3

> (mod 25 5) 0

The first argument is divisible by the second if the remainder is zero. The function leap? uses this technique to determine whether its argument is a leap year:

> (mapcar #'leap? '(1904 1900 1600)) (T NIL T)

The function we'll use to convert dates to integers is date->num. It returns the sum of the values for each component of a date. To find the number of days up to the start of the month, it calls month-num, which looks in month, then adds 1 if the month is after February in a leap year.

To find the number of days up to the start of the year, date->num calls year-num, which returns the integer representing January 1 of that year. This function works by counting up or down from the year y given as an argument toward year zero (2000).

Figure 5.2 shows the second half of the code. The function num->date converts integers back to dates. It calls num-year, which returns the year in the date, and the number of days left over. It passes the latter to num-month, which extracts the month and day.

Like year-num, num-year counts up or down from year zero, one year at a time. It accumulates days until it has a number whose absolute value is greater than or equal to that of n. If it was counting down, then it can return the values from the current iteration. Otherwise it will overshoot the year, and must return the values from the previous iteration. This is the point of prev, which on each iteration will be given the value that days had on the previous iteration.

The function num-month and its subroutine nmon behave like month-num in reverse. They go from value to position in the constant vector month, while month-num goes from position to value.

The first two functions in Figure 5.2 could have been combined in one.

Instead of returning values to another function, num-year could invoke num-month directly. The code is easier to test interactively when it's broken up like this, but now that it works, the next step might be to combine them.

With date->num and num->date, date arithmetic is easy.0 We use them as in date+, which can add or subtract days from a date. If we ask date+ for the date 60 days from December 17,1997,

> ( m u l t i p l e - v a l u e - l i s t (date+ 17 12 1997 60)) (15 2 1998)

we get February 15,1998.

(defun num->date (n)

(multiple-value-bind (y left) (num-year n) (multiple-value-bind (m d) (num-month left y)

(values d m y)))) (defun num'-year (n)

(if (< n 0)

(do* ((y (- yzero 1) (- y 1))

(d (- (year-days y)) (- d (year-days y)))) ((<= d n) (values y (- n d))))

(do* ((y yzero (+ y 1)) (prev 0 d)

(d (year-days y) (+ d (year-days y)))) ((> d n) (values y (- n prev)))))) (defun num-month (n y)

(if (leap? y)

(Gond ((= n 59) (values 2 29)) ((> n 59) (nmon (- n 1))) (t (nmon n))) (nmon n)))

(defun nmon (n)

(let ((m (position n month :test #*<)))

(values m (+ 1 (- n (svref month (- m 1))))))) (defun date-*- (d m y n)

(num->date (+ (date->num d m y) n)))

Figure 5.2: Date arithmetic: Converting integers to dates.

Summary

1. Common Lisp has three basic block constructs: progn; block, which allows returns; and tagbody, which allows gotos. Many built-in oper-ators have implicit blocks.

2. Entering a new lexical context is conceptually equivalent to a function call.

3. Common Lisp provides conditionals suited to various situations. All can be denned in terms of if.

EXERCISES 97 4. There is a similar variety of operators for iteration.

5. Expressions can return multiple values.

6. Computations can be interrupted, and protected against the conse-quences of interruption.

Exercises

1. Translate the following expressions into equivalent expressions that don't use l e t or l e t * , and don't cause the same expression to. he evaluated twice.

(a) ( l e t ((x (car y ) ) ) (cons x x)) (b) ( l e t * ((w (car x))

(y (+ v z ) ) ) (cons w y))

2. Rewrite mystery (page 29) to use cond.

3. Define a function that returns the square of its argument, and which does not compute the square if the argument is a positive integer less than or equal to 5.

4. Rewrite num-month (Figure 5.1) to use case instead of svref.

5. Define iterative and recursive versions of a function that takes an object x and vector v, and returns a list of all the objects that immediately precede x in v:

> (precedes # \ a "abracadabra") (#\c #\d # \ r )

6. Define iterative and recursive versions of a function that takes an object and a list, and returns a new list in which the object appears between each pair of elements in the original list:

> (intersperse ' - ' ( a b e d ) ) (A - B - C - D)

7. Define a function that takes a list of numbers and returns true iff the difference between each successive pair of them is 1, using

(a) recursion (b) do

(c) mapc and r e t u r n

8. Define a single recursive function that returns, as two values, the max-imum and minmax-imum elements of a vector.

9. The program in Figure 3.12 continues to search as the first complete path works its way through the queue. In broad searches this would be a problem.

(a) Using c a t c h and throw, modify the program to return the first complete path as soon as it is discovered.

(b) Rewrite the program to do the same thing without using catch and throw.

Dans le document ANSI Common Lisp (Page 109-116)