You want to convert spaces in a string to tabs, or vice versa.
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 '()) (len-so-far 0) (start-run 0) (src-index (string-index str #\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))))))))
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))) (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