miércoles, 17 de abril de 2019

From HtDP to Racket. Racket (1): #lang, local definitions, minor tweaks

The very first thing we need to do in order to change from ISL to Racket is to select the appropriate language via the DrRacket's menu 'Language:Choose Language'.

We will choose #lang racket/base. You can also select #lang racket. The first option loads the base of Racket, and everything else has to be required on demand. The second option loads the core and many other libraries.

With this addition the racket/base requirement of our previous version is, of course, no longer needed.

Before trying to run the code, please comment out all tests. We will look into them in the next post.

If you run the code now you will get a syntax error. This is expected. ISL+ is not Racket.

A first quick fix is to require racket/list, which provides functions like range, empty?, and first used in our prior implementation.

Another simple fix has to do with symbol=?. symbol=? is provided by racket/bool. You could require that module. Or, as we are going to do, use the more generic equal?. In general, though, it is better to use a specific function like symbol=?.

explode is another function only available in teaching languages. You have two options here: implementing your own version of explode or refactoring the function that uses explode, isbn-string->numbers. We choose the second approach, that iterates now on Char's instead of on 1String's.

A more serious modification has to do with local definitions. This is an *SL construct. Most of the time you should use a simple define instead, but you may need let forms in other special cases.

The resulting transformation looks as follows:

; ----------------------------------------------------------
; isbn-racket.v1.rkt
; - local -> define
; - require racket/list
; - isbn-string->numbers refactored
; - symbol=? -> equal? [or require racket/bool]
; ----------------------------------------------------------

#lang racket/base

(require racket/list)   ;empty?, first, range
(require racket/string) ;string-normalize-spaces
                        ;string-replace
                        ;string-split
;(require racket/file) ;file->string (for tests)

; ----------------------------------------------------------
; Data Types

; An ISBN is one of:
; - ISBN-13
; - ISBN-10

; An ISBN-String is one of:
; - ISBN-13-String
; - ISBN-10-String

; An ISBN-13 is an valid ISBN-13-String
; An ISBN-10 is an valid ISBN-10-String
; where valid means that the numbers in that string
; fullfil a certain mathematical computation. (See
; ISBN User Manual for information about this computation)
(define isbn-13-ex "9781593274917")
(define isbn-10-ex "0262062186")

; An ISBN-13-String is a String consisting of 13 digits,
; where a digit is one of '0', '1', ..., '9'.
(define isbn-13-str-ex "1234567890123")

; An ISBN-10-String is a String consisting of 9 digits,
; and a last letter that can be a digit or 'X'.
(define isbn-10-str-ex-1 "1234567890")
(define isbn-10-str-ex-2 "123456789X")

; An ISBN-Format is one of:
; - 'isbn-13
; - 'isbn-10

; ----------------------------------------------------------
; Patterns and Regexes
; [See ISBN International User Manual 7e. Sect. 5]

; - Pattern Components
(define pat-isbn-sep "[ -]")
(define pat-isbn-id "ISBN(-1[03])?:? ")
(define pat-isbn-13-id "(?:ISBN(?:-13)?:? )?")
(define pat-isbn-10-id "(?:ISBN(?:-10)?:? )?")
(define pat-isbn-prefix "97[89][ -]")
(define pat-isbn-registration "\\d{1,5}[ -]")
(define pat-isbn-registrant "\\d{1,7}[ -]")
(define pat-isbn-publication "\\d{1,6}[ -]")
(define pat-isbn-13-check "\\d")
(define pat-isbn-10-check "[X\\d]")

; - Look ahead to ISBN groups
(define pat-isbn-13-look-ahead
  (string-append "(?=" pat-isbn-prefix pat-isbn-registration ")"))

(define pat-isbn-10-look-ahead
  (string-append "(?=" pat-isbn-registration ")")) 

; - Main patterns
(define pat-isbn-13/groups
  (string-append pat-isbn-13-id
                 pat-isbn-13-look-ahead
                 pat-isbn-prefix
                 pat-isbn-registration
                 pat-isbn-registrant
                 pat-isbn-publication
                 pat-isbn-13-check))

(define pat-isbn-10/groups
  (string-append pat-isbn-10-id
                 pat-isbn-10-look-ahead
                 pat-isbn-registration
                 pat-isbn-registrant
                 pat-isbn-publication
                 pat-isbn-10-check))

(define pat-isbn-13/prefix
  (string-append pat-isbn-13-id pat-isbn-prefix "\\d{10}"))

(define pat-isbn-13-norm "\\d{13}")

(define pat-isbn-10-norm "\\d{9}[X\\d]")

(define pat-isbn-13
  (string-append pat-isbn-13-norm "|"
                 pat-isbn-13/prefix "|"
                 pat-isbn-13/groups))

(define pat-isbn-10
  (string-append pat-isbn-10-norm "|"
                 pat-isbn-10/groups))

(define pat-isbn
  (string-append pat-isbn-13 "|"
                 pat-isbn-10))

; - Regexes
(define re-isbn-id (regexp pat-isbn-id))
(define re-isbn-sep (regexp pat-isbn-sep))
(define re-isbn-13 (pregexp pat-isbn-13))
(define re-isbn-10 (pregexp pat-isbn-10))
(define re-isbn-13-norm (pregexp pat-isbn-13-norm))
(define re-isbn-10-norm (pregexp pat-isbn-10-norm))
(define re-isbn (pregexp pat-isbn))

; ----------------------------------------------------------
; Predicates
#|
(check-expect (isbn? "9781593274917") #t)
(check-expect (isbn? "0262062186") #t)
(check-expect (isbn? #f) #f)
  
(check-expect (isbn-13? "9781593274917") #t)
(check-expect (isbn-13? "0262062186") #f)
(check-expect (isbn-13? "") #f)
  
(check-expect (isbn-10? "9781593274917") #f)
(check-expect (isbn-10? "0262062186") #t)
(check-expect (isbn-10? 1) #f)
  
(check-expect (isbn-string? "9781593274912") #t)
(check-expect (isbn-string? "026206218X") #t)
(check-expect (isbn-string? "97815932749122") #f) ;too long
(check-expect (isbn-string? "978159327491") #f)   ;too short
(check-expect (isbn-string? "0262062189X") #f)    ;too long
(check-expect (isbn-string? "026206218") #f)      ;too short
(check-expect (isbn-string? "0-262-06218-6") #f)
  
(check-expect (isbn-13-string? "9781593274912") #t)
(check-expect (isbn-13-string? "97815932749122") #f) ;too long
(check-expect (isbn-13-string? "978159327491") #f)   ;too short
(check-expect (isbn-13-string? #f) #f)
  
(check-expect (isbn-10-string? "026206218X") #t)
(check-expect (isbn-10-string? "0262062189X") #f) ;too long
(check-expect (isbn-10-string? "026206218") #f)   ;too short
(check-expect (isbn-10-string? #f) #f)
  
(check-expect (isbn-format? 'isbn-13) #t)
(check-expect (isbn-format? 'isbn-10) #t)
(check-expect (isbn-format? "isbn-10") #f)
|#  
(define (isbn? v)
  (or (isbn-13? v) (isbn-10? v)))

(define (isbn-13? v)
  (and (isbn-13-string? v) (isbn-13-valid? v)))

(define (isbn-10? v)
  (and (isbn-10-string? v) (isbn-10-valid? v)))

(define (isbn-string? v)
  (or (isbn-13-string? v) (isbn-10-string? v)))

(define (isbn-13-string? v)
  (and (string? v) (regexp-match-exact? re-isbn-13-norm v)))

(define (isbn-10-string? v)
  (and (string? v) (regexp-match-exact? re-isbn-10-norm v)))

(define (isbn-format? v)
  (or (equal? v 'isbn-13) (equal? v 'isbn-10)))

; ----------------------------------------------------------
; ISBN Validation

; ISBN-13-String -> Boolean
; is the given isbn-13 string a valid isbn-13
#|
(check-expect (isbn-13-valid? "9781593274917") #t)
(check-expect (isbn-13-valid? "9780201896831") #t)
(check-expect (isbn-13-valid? "9781593274912") #f)
(check-expect (isbn-13-valid? "9780201896834") #f)
|#
(define (isbn-13-valid? isbn-str)
  (isbn-checksumf (isbn-string->numbers isbn-str)
                  '(1 3 1 3 1 3 1 3 1 3 1 3 1)
                  10))

; ISBN-10-String -> Boolean
; is the given isbn-10 string a valid isbn-10
#|
(check-expect (isbn-10-valid? "0262062186") #t)
(check-expect (isbn-10-valid? "026256114X") #t)
(check-expect (isbn-10-valid? "026206218X") #f)
(check-expect (isbn-10-valid? "0262561141") #f)
|#
(define (isbn-10-valid? isbn-str)
  (isbn-checksumf (isbn-string->numbers isbn-str)
                  (range 10 0 -1)
                  11))

; [List-of N] [List-of N] N -> Boolean
; abstract checksum algorithm for isbn validation
#|
(check-expect (isbn-checksumf '(9 7 8 1 5 9 3 2 7 4 9 1 7)
                              '(1 3 1 3 1 3 1 3 1 3 1 3 1)
                              10) #t)
(check-expect (isbn-checksumf '(0 2 6 2 0 6 2 1 8 10)
                              (range 10 0 -1)
                              11) #f)
|#
(define (isbn-checksumf multiplicands multipliers mod)
  (define sum (foldl + 0 (map * multiplicands multipliers)))
  (zero? (modulo sum mod)))

; ISBN-String -> [List-of N]
; translates str into the numbers their isbn letters represent
#|
(check-expect (isbn-string->numbers "026256114X")
              '(0 2 6 2 5 6 1 1 4 10))
|#
(define (isbn-string->numbers str)
  (define (isbn-digit->number char)
    (cond
      [(char=? char #\X) 10]
      [else (string char)]))
  (map isbn-digit->number (string->list str)))

; ----------------------------------------------------------
; ISBN Extraction

; String -> [List-of ISBN]
; extracts all isbns from str
#|  
(check-expect (isbn-find/list "") '())
(check-expect (isbn-find/list "none") '())
(check-expect
 (isbn-find/list (file->string "test-isbn-examples"))
 (list
  ;isbn normalized
  "0262062186" "026256114X" "1593274912"
  "9781593274917" "0201896834" "9780201896831"
  ;isbn w/ several id's a sep's
  "0262062186" "026256114X" "0262062186" "0201896834"
  "9780201896831" "026256114X" "9780201896831"))
|#
(define (isbn-find/list str)
  (define candidates
    (foldr append '()
           (map isbn-match* (string-split str "\n"))))
  (filter isbn? (map isbn-normalize candidates)))

; String ISBN-Format -> [Maybe ISBN]
; extracts the first isbn of given format from str, if any
#|
(check-expect (isbn-find "" 'isbn-13) #f)
(check-expect (isbn-find "" 'isbn-10) #f)
(check-expect (isbn-find "0262062186" 'isbn-13) #f)
(check-expect (isbn-find "9781593274917" 'isbn-10) #f)
(check-expect (isbn-find (file->string "test-isbn-examples")
                         'isbn-13)
              "9781593274917")
(check-expect (isbn-find (file->string "test-isbn-examples")
                         'isbn-10)
              "0262062186")
|#
(define (isbn-find str format)
  (define p?
    (cond [(equal? format 'isbn-13) isbn-13?]
          [(equal? format 'isbn-10) isbn-10?]))
  (define isbns
    (filter p? (isbn-find/list str)))
  (cond
    [(empty? isbns) #f]
    [else (first isbns)]))
    
; ----------------------------------------------------------
; Helpers

; String -> [List-of String]
; matches substrings in the given string looking like isbn tags
#|
(check-expect (isbn-match* "") '())
(check-expect (isbn-match* "abc\nd") '())
(check-expect
 (isbn-match* (file->string "test-isbn-examples"))
 (list
  ;isbn normalized (all matched)
  "0262062186" "026256114X" "1593274912"
  "9781593274917" "0201896834" "9780201896831"
  ;isbn w/ several id's and sep's (all matched)
  "ISBN 0-262-06218-6"
  "ISBN: 0 262 56114 X"
  "ISBN-10 0 262 06218-6"
  "ISBN-10: 0-201-89683-4"
  "ISBN-13: 978-0-201-89683-1"
  "ISBN-10: 0 262 56114 X"
  "ISBN-13: 978-0201896831"
  ;not isbn strings (usually impossible in real-world)
  "026206218X" "0262561141" "9780201896834" ;partially matched
  ;isbn strings, but isbn invalid (all matched)
  "026206218X" "0262561141" "1593274913"
  "9781593274912" "0201896833" "9780201896834"))
|#
(define (isbn-match* str)
  (regexp-match* re-isbn (string-normalize-spaces str)))

; String -> String
; removes the isbn-id and, then, the isbn separators from str
#|
(check-expect (isbn-normalize "123-45 67") "1234567")
(check-expect (isbn-normalize "ISBN 123") "123")
(check-expect (isbn-normalize "ISBN: 123") "123")
(check-expect (isbn-normalize "ISBN-10 123") "123")
(check-expect (isbn-normalize "ISBN-10: 123") "123")
(check-expect (isbn-normalize "ISBN-13 123") "123")
(check-expect (isbn-normalize "ISBN-13: 123") "123")
(check-expect (isbn-normalize "ISBN-14: hi") "ISBN14:hi")
|#
(define (isbn-normalize str)
  (string-replace
   (string-replace str re-isbn-id "") re-isbn-sep ""))

Next article in the series: Racket: only-in, rackunit, test submodules

No hay comentarios:

Publicar un comentario