s c h e m a t i c s : c o o k b o o k

/ StringChapter / Cookbook.StringTabs

This Web


WebHome 
WebChanges 
TOC (with recipes)
NewRecipe 
WebTopicList 
WebStatistics 

Other Webs


Chicken
Cookbook
Erlang
Know
Main
Plugins
Sandbox
Scm
TWiki  

Schematics


Schematics Home
Sourceforge Page
SchemeWiki.org
Original Cookbook
RSS

Scheme Links


Schemers.org
Scheme FAQ
R5RS
SRFIs
Scheme Cross Reference
PLT Scheme SISC
Scheme48 SCM
MIT Scheme scsh
JScheme Kawa
Chicken Guile
Bigloo Tiny
Gambit LispMe
GaucheChez

Lambda the Ultimate
TWiki.org

Expanding and Compressing Tabs

Problem

You want to convert spaces in a string to tabs, or vice versa.

Solution

You can define this by searching for tabs and expanding them as they're found.
(define expand-tabs
  (opt-lambda (str (tabstop 8))
    (let loop ((result-parts '()) ; parts of the expanded string
               (len-so-far 0) ; sum of the string-length of result-parts
               (start-run 0) ; start of the next run of non-tab characters
               (src-index (string-index str #\tab))) ; index of the next #\tab
      (if (not src-index)
          (foldl string-append "" (cons (substring str start-run) result-parts))
          (let* ((current-length (+ (- src-index start-run) len-so-far))
                 (tab-len (- tabstop (modulo current-length tabstop))))
            (loop (cons (make-string tab-len #\space) 
                        (cons (substring str start-run src-index) result-parts))
                  (+ tab-len current-length)  
                  (+ src-index 1)   
                  (string-index str #\tab (+ 1 src-index))))))))

Discussion

Python includes an expandtabs() method, but Scheme doesn't. This is probably because the tab/space distinction is very important in Python source code, but not so in Scheme. Still, it's a useful thing to do sometimes. The tricky thing about tab expansion is that a tab doesn't translate directly into a fixed number of spaces; you have to calculate the number of spaces for each tab to reach the next tabstop. Tabstops divide the string into equal partitions of the length specified, and the action of a tab character is to move forward to the next tabstop. For example, if your tabstop is 4 and you have the string "12345\t678\t", the first tab converts to 3 spaces and the 2nd converts to one space, e.g. "12345 678 ". This solution uses a named let to iterate over the string, building up alternate sequences of spaces and non-tab runs of characters. On each loop, if we've reached the end of the string without finding another tab, then we append the remainder of the string to the other parts and fold them together with string-append. Otherwise, we calculate the number of spaces that this tab expands to, then loop with the tab expansion and the current run of non-tab characters appended onto the result list.

-- GordonWeakliem - 27 Apr 2004

This implementation feels too complicated, but I tried a number of approaches and this was the only correct one I could come up with.

-- GordonWeakliem - 27 Apr 2004

Here's an alternate that's a bit different.

(define expand-tabs
  (opt-lambda (str (tabstop 8))
    (let loop ((parts (regexp-split "\t" str))
               (result-parts '())
               (len-so-far 0))
      (if (null? parts)
          (foldr string-append "" (reverse (cdr result-parts)))
          (let* ((part (car parts))
                 (next-tabstop (- tabstop (modulo len-so-far tabstop))) ; distance to the next tabstop
                 (len-so-far (+ len-so-far (string-length part))))
            (loop (cdr parts) 
                  (cons (string-append part (make-string (- tabstop (modulo len-so-far tabstop)) #\space)) result-parts) 
                  (+ len-so-far (string-length part))))))))
-- GordonWeakliem - 28 Apr 2004

Gordon, do you have any test cases for the above procedure. I'd like to try implementing my own version but the output of your versions isn't what I expected, so I'm not sure of the desired functionality.

-- NoelWelsh - 30 Apr 2004

Here's the test cases. I generated the expected values by running them through Python's expandtabs(), the tabstop is 8.

(test/text-ui
 (make-test-suite
  "Expand Tabs Tests"
(make-test-case "No tab" (assert-equal? (expand-tabs "1234567890") "1234567890"))
 (make-test-case "Leading Tab" (assert-equal? (expand-tabs "\t1234567890") "        1234567890"))
 (make-test-case "Trailing Tab" (assert-equal? (expand-tabs "1234567890\t") "1234567890      "))
 (make-test-case "Tabs in middle" (assert-equal? (expand-tabs "1\t2\t34567890") "1       2       34567890"))
 (make-test-case "Tab + Space" (assert-equal? (expand-tabs "1\t2 \t34567890") "1       2       34567890"))
 (make-test-case "Tabs in middle #2" (assert-equal? (expand-tabs "123\t4567\t890") "123     4567    890"))
 (make-test-case "Leading and inner tabs" (assert-equal? (expand-tabs "\t123\t4567\t890") "        123     4567    890"))
 (make-test-case "Trailing and inner tabs" (assert-equal? (expand-tabs "123\t4567\t890\t") "123     4567    890     "))
 (make-test-case "Leading, trailing and inner tabs" (assert-equal? (expand-tabs "\t123\t4567\t890\t") "        123     4567    890     "))
 (make-test-case "2 trailing tabs" (assert-equal? (expand-tabs "\t123\t4567\t890\t\t") "        123     4567    890             "))
 (make-test-case "2 leading & 2 trailing tabs" (assert-equal? (expand-tabs "\t\t123\t1234567890\t\t") "                123     1234567890              "))
 ))

-- GordonWeakliem - 30 Apr 2004

Below is my solution. BTW, there was a superfluous make-test-suite in the test suite above. I've removed it.

(require (lib "plt-match.ss")
         (lib "etc.ss")
         (lib "13.ss" "srfi"))

(define expand-tabs
  (opt-lambda (str (tabstop 8))
    (define (tab? char)
      (eq? #\tab char))
    (define (make-expanded-tab position)
     (make-string (- tabstop (modulo position tabstop)) #\space))
    
    (apply string-append
           (reverse!
            (vector-ref 
             (string-fold
              (lambda (char seed)
                (match seed
                  [(vector position result)
                   (if (tab? char)
                       (let ((insert (make-expanded-tab position)))
                         (vector (+ (string-length insert) position)
                                 (cons insert result)))
                       (vector (add1 position)
                               (cons (string char) result)))]))
              (vector 0 (list))
              str)
             1)))))

To make this solution faster, an efficient growable vector would be a good replacement for the list used in result. It would be interesting to benchmark the current solutions.

-- NoelWelsh - 07 May 2004

This particular topic looks more like a reusable library call than a code pattern to me...

(require (lib "tabexpand.ss" "tabexpand"))

http://www.neilvandyke.org/tabexpand-scm/ :)

-- NeilVanDyke - 09 May 2004

I removed my versions. They were solving the wrong problem.

-- JensAxelSoegaard - 09 May 2004

CookbookForm
TopicType: Recipe
ParentTopic: StringRecipes
TopicOrder: 080

 
 
Copyright © 2004 by the contributing authors. All material on the Schematics Cookbook web site is the property of the contributing authors.
The copyright for certain compilations of material taken from this website is held by the SchematicsEditorsGroup - see ContributorAgreement & LGPL.
Other than such compilations, this material can be redistributed and/or modified under the terms of the GNU Lesser General Public License (LGPL), version 2.1, as published by the Free Software Foundation.
Ideas, requests, problems regarding Schematics Cookbook? Send feedback.
/ You are Main.guest