Click here to Skip to main content
15,889,216 members
Please Sign up or sign in to vote.
5.00/5 (4 votes)
See more:
Your coding challenge of the week is a simple one. Or so it seems.

Given a text string, convert the tabs to spaces. Assuming tab markers are 4 characters apart

-text
[ ][ ][tab]Hello, world!

becomes
-text
[ ][ ][ ][ ]Hello, World!


Where [ ] = space character, [tab] = tab character (I chose to use these instead of " " and " " (or "\t") to make it easier to spot the spaces and tabs.)

Another test case is left as an exercise for the reader.
-text
[ ][ ][ ][tab][tab][ ][tab]Hello, world!



Again: A T-shirt awarded for audacity, for style, for outrageous use of inappropriate languages. Mostly for the entry that entertains us the most.

Last week's challenge Coding challenge: bad word filter[^] was won by PIEBALDConsult under the "God I miss coding" category. Send Sean @codeprject your address and T-shirt size and a Christmas present will come your way.

What I have tried:

To remain calm, mostly.

Got a coding challenge idea? Send it to me at chris@codeproject.com.
Posted
Updated 6-Dec-16 21:24pm
v3
Comments
Afzaal Ahmad Zeeshan 1-Dec-16 17:27pm    
Do you mean tab spaces, or just tab as in character?
CHill60 2-Dec-16 4:45am    
"outrageous use of inappropriate languages" ... darn it, I was going to provide a VB6 solution then remembered I destroyed the install CDs in a ritual bonfire last Samhain
Graeme_Grant 2-Dec-16 7:04am    
Any reason you are converting words to Title Case as well???
PIEBALDconsult 2-Dec-16 8:49am    
Been there, done that. I'll resurrect some ancient code.

And don't forget this little beauty...
https://www.codeproject.com/Articles/32593/Untabify-and-Tabify

Patrice T 3-Dec-16 15:28pm    
I think some solutions will fail with:
"[ ][ ][ ][ ][ ][tab]Hello, world!"

Page 2 of 2


Javascript, using the replace() function, without using recursion (as in Solution 7). Also, I use a lookup table rather than looping to append the spaces.

JavaScript
tabs = ['    ', '   ', '  ', ' '];
function converTabs(aString) {
     console.log('source: ' + visualizeWS(aString));
     var extraChars = 0;
     var result = aString.replace(/\t/g, function (match, position) {
         var positionInResult = position + extraChars;
         var replacement = tabs[positionInResult % 4];
         extraChars += replacement.length - 1;
         return replacement;
    });
    console.log('result: ' + visualizeWS(result));
};


Function to display results:
JavaScript
function (aString) {
    var result = "";
    for (var index = 0, limit = aString.length; index < limit; index++) {
        var aChar = aString[index];
        if (aChar === ' ')
            result += "[ ]";
        else if (aChar === '\t')
            result += "[tab]";
        else
            result += aChar;
    }
    return result;
};


Output:
source: [ ][ ][tab]Hello,[ ]World!
result: [ ][ ][ ][ ]Hello,[ ]World!

source: [ ][ ][ ][tab][tab][ ][tab]Hello,[ ]World!
result: [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]Hello,[ ]World!
 
Share this answer
 
Just for fun, how about FoxPro?

Convert tabs to 4 spaces or less, as here:
* Input: [ ][ ][ ][tab][tab][ ][tab]Hello, world!
* Result: [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]Hello, world!
*
* Using all single letter variables just to tick everyone off :)
*
* Clear the screen
CLEAR

* This is the source string
s = '   		 	Hello, world!'
* Display the original
?s
r = ''
n = 0

* Count number of TABs
FOR x = 1 TO LEN(s)
	n = n + IIF(ASC(SUBSTR(s, x, 1)) = 9, 1, 0)
ENDFOR

* Get rid of all spaces and TABs beginning and end of s
r = ALLTRIM(s, 1, ' ', CHR(9))
* Add appropriate number of spaces
r = SPACE(n*4) + r

* Display the Actual results
?r

* Prettify it so Chris can read it
r = ALLTRIM(r, 1, ' ')
x = LEN(r)
t = PADL(r, x + ((n*4)*3), '[ ]')
?t


Output:

Original String:
 		 	Hello, world!
Tabified String:
          Hello, world!
Prettified Tabified String for Chris:
[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]Hello, world!
 
Share this answer
 
v2
Actually really simple with Regular Expressions. Just consume the extraneous spaces, then replace all tabs with the correct number of spaces. There's probably optimizations and tricks I missed, but it's Friday, and really can't be arsed.

Edit: Just occurred to me that the intermediate step was unnecessary. You can just just replace with the correct number of spaces without removing spaces first. Improved code below.

Code (C#)
C#
using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace TabsToSpaces
{
    public static class TabsToSpaceReplacer
    {
        private const int _defaultTabWidth = 4;

        private static string tabWidthToken = @"[TWT]";

        private static string tabToken = @"[tab]";
        private static string spaceToken = @"[ ]";

        private static string escapedTabToken = Regex.Escape(tabToken);
        private static string escapedSpaceToken = Regex.Escape(spaceToken);

        private static string spaceReplacerRegex =
            @"(" + escapedSpaceToken + "){0," + tabWidthToken + @"}" + escapedTabToken;
        

        public static string TabsToSpaces(
            this string input,
            int tabWidth = _defaultTabWidth)
        {
            string finalRegex =
                spaceReplacerRegex.Replace(tabWidthToken, tabWidth.ToString());

            //return
            //    Regex.Replace(
            //        Regex.Replace(input, finalRegex, tabToken),
            //        escapedTabToken,
            //        string.Concat(Enumerable.Repeat(spaceToken, tabWidth)));

            //Improved version
            return Regex.Replace(
                input,
                finalRegex,
                string.Concat(Enumerable.Repeat(spaceToken, tabWidth)));
        }
    }
}


Tests
C#
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using TabsToSpaces;

namespace UnitTestProject1
{
    [TestClass]
    public class UnitTest1
    {
        private List<tuple<string, string="">> testStrings = new List<tuple<string,string>>()
            {
                new Tuple<string, string="">
                    (@"[ ][ ][tab]Hello, world!",
                     @"[ ][ ][ ][ ]Hello, world!"),

                new Tuple<string, string="">
                    (@"[ ][ ][ ][tab][tab][ ][tab]Hello, world!",
                     @"[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]Hello, world!"),

                new Tuple<string, string="">
                    (@"[ ][tab][ ][tab][tab][ ][ ]Hello, world!",
                     @"[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]Hello, world!"),

                new Tuple<string, string="">
                    (@"[tab][ ][tab][ ][ ][tab]Hello, world!",
                     @"[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]Hello, world!"),

                new Tuple<string, string="">
                    (@"[tab][tab][tab][tab]Hello, world!",
                     @"[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]Hello, world!"),

                new Tuple<string, string="">
                    (@"[ ][ ][ ][ ]Hello, world!",
                     @"[ ][ ][ ][ ]Hello, world!")
            };

        private int TestTabWidth = 4;

        [TestMethod]
        public void TestMethod1()
        {
            bool result = true;

            foreach (Tuple<string, string=""> pair in testStrings){
                Console.WriteLine(string.Format("Input:    {0}", pair.Item1));
                string output = pair.Item1.TabsToSpaces(TestTabWidth);
                Console.WriteLine(string.Format("Expected: {0}", pair.Item2));
                Console.WriteLine(string.Format("Output:   {0}", output));

                result = !result ? result : string.Equals(pair.Item2, output);
            }

            Assert.IsTrue(result);
        }
    }
}


Console Output
Test Name:	TestMethod1
Test Outcome:	Passed
Result StandardOutput:	
Input:    [ ][ ][tab]Hello, world!
Expected: [ ][ ][ ][ ]Hello, world!
Output:   [ ][ ][ ][ ]Hello, world!
Input:    [ ][ ][ ][tab][tab][ ][tab]Hello, world!
Expected: [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]Hello, world!
Output:   [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]Hello, world!
Input:    [ ][tab][ ][tab][tab][ ][ ]Hello, world!
Expected: [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]Hello, world!
Output:   [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]Hello, world!
Input:    [tab][ ][tab][ ][ ][tab]Hello, world!
Expected: [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]Hello, world!
Output:   [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]Hello, world!
Input:    [tab][tab][tab][tab]Hello, world!
Expected: [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]Hello, world!
Output:   [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]Hello, world!
Input:    [ ][ ][ ][ ]Hello, world!
Expected: [ ][ ][ ][ ]Hello, world!
Output:   [ ][ ][ ][ ]Hello, world!
 
Share this answer
 
v2
Of course we also must have a Test Driven Development-in-a-hurry solution, so first write test according to the customer's wishes and also add a "negative" test to get full statement and decision coverage to satisfy the formal unit test checklist and code coverage criteria:
C#
[TestMethod]
public void TestDrivenToMadnessDevelopmentTest()
{
     Assert.AreEqual("[ ][ ][ ][ ]Hello, World!", "[ ][ ][tab]Hello, world!".ExpandTabs());
     Assert.AreEqual("Hello", "Hello".ExpandTabs()));
}

and then remember to first implement it so that it fails:
C#
public static String ExpandTabs(this string text)
{
     throw new NotImplementException();
}

Finally implement the "working" code like this:
C#
public static String ExpandTabs(this string text)
{
     if (text == "[ ][ ][tab]Hello, world!") return "[ ][ ][ ][ ]Hello, World!"; // To pass first unit test :-)
     return text;  // To pass second test, and perhaps some other tests if we are lucky :-)
}

Done in less than a quarter! Time to check in the code and get on with the next challenge. :-)
 
Share this answer
 
Comments
Jon McKee 3-Dec-16 20:14pm    
I want to be mad because of how true this can be but I can't stop laughing. Bravo /clap
I've been needing one of these in my library anyway.

// ------------------------------------------------- //

C#
/// <summary>
/// Convert tabs in the input string to spaces.
/// </summary>
/// <param name="InputBuffer">String with imbedded tabs to convert</param>
/// <param name="TabWidth">The width of the tab character.</param>
/// <returns></returns>
///
public string TabConvert(String InputBuffer, int TabWidth) {
    var outBuf = new StringBuilder();

    var inBufArray = InputBuffer.ToCharArray();
    int destinationIndex = 0;
    char c;

    for (int i = 0; i < inBufArray.Length; i++) {
        c = inBufArray[i];
        if (c == '\t') {
            do {
                outBuf.Append(' ');
            } while ((++destinationIndex % TabWidth) != 0);
        }
        else {
            outBuf.Append(c);
            ++destinationIndex;
        }
    }

    return outBuf.ToString();
}
 
Share this answer
 
C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Globalization;
using System.Threading;

namespace TabConversion
{
    class Program
    {
        static void Main(string[] args)
        {
            CultureInfo cultureInfo = Thread.CurrentThread.CurrentCulture;
            TextInfo textInfo = cultureInfo.TextInfo;

            //here assumed that spaces from the begining and at the end are removed before replacing tab character
            string  sample= "   \t\t \tHello, world!"; //Output: "[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]Hello, world!"
            Console.WriteLine(textInfo.ToTitleCase( sample.Trim().RemoveTab()));

            //This loop will process strings passsed as command line parameters
            foreach (string s in args)
            {
                if (s.IndexOf('\t') >= 0)
                    Console.WriteLine(textInfo.ToTitleCase(s.Trim().RemoveTab()));
            }

            do{
                Console.WriteLine("Type new string to remove tab (Q to exit):");
                sample = Console.ReadLine();
                if (sample.IndexOf('\t') >= 0)
                    Console.WriteLine(textInfo.ToTitleCase( sample.Trim().RemoveTab()));
                else if(sample.ToLower() != "q")
                    Console.WriteLine(string.Format("Tab not found in \"{0}\"",sample));
            } while (sample.ToLower() != "q") ;
        }
    }

     public static class StringExtension
    {
        /// <summary>
        /// Removes tab and replaces with 4 spaces
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        public static string RemoveTab(this string str)
        {
            return str.RemoveTab(4);
        }

        /// <summary>
        /// Replaces tab with spaces specified by SpaceCount
        /// </summary>
        /// <param name="str"></param>
        /// <param name="spaceCount"></param>
        /// <returns></returns>
        public static string RemoveTab(this string str, int SpaceCount)
        {
            return str.RemoveTab(str.Space(SpaceCount));
        }

        /// <summary>
        /// Remove tab and replace it with value passed as ReplaceWithString
        /// </summary>
        /// <param name="str"></param>
        /// <param name="ReplaceWithString"></param>
        /// <returns></returns>
        public static string RemoveTab(this string str, string ReplaceWithString)
        {
            return str.Replace("\t", ReplaceWithString);
        }

        /// <summary>
        /// Returns string of space with the length specified as SpaceCount
        /// </summary>
        /// <param name="str"></param>
        /// <param name="SpaceCount"></param>
        /// <returns></returns>
        public static string Space(this string str, int SpaceCount)
        {
            return str.Repeat(' ', SpaceCount);
        }

        /// <summary>
        /// Returns string of the character specified as C repeating count times 
        /// </summary>
        /// <param name="str"></param>
        /// <param name="c"></param>
        /// <param name="count"></param>
        /// <returns></returns>
        public static string Repeat(this string str, char c, int count)
        {
            return new string(c, count);
        }
    }
}
 
Share this answer
 
My solution using xHarbour, xBase family like dBase, Clipper or FoxPro.
Note that the language integrate an automatic StringBuilder feature making concatenation always efficient.
VB
clear
tab= chr(9)
test_in="  "+tab+"Hello, world!"
? "Input : "
print(test_in)
? "Output: "
test_out= untab(test_in)
print(test_out)
?

test_in="   "+tab+tab+" "+tab+"Hello, world!"
test_out= untab(test_in)
? "Input : "
print(test_in)
? "Output: "
print(test_out)
?

test_in=tab+"a"+tab+"ab"+tab+"abc"+tab+"Hello, world!"
test_out= untab(test_in)
? "Input : "
print(test_in)
? "Output: "
print(test_out)
?
return

procedure print (str)
	for scan= 1 to len(str)
		if str[scan]= tab
			?? "[tab]"
		elseif str[scan]= " "
			?? "[ ]"
		else
			?? str[scan]
		endif
	next
	return

function untab(str)
	out= ""
	pos= 0
	for scan= 1 to len(str)
		if str[scan]= tab
			out += spaces(4-(pos % 4))
			pos += 4-(pos % 4)
		else
			out += str[scan]
			pos ++
		endif
	next
	return out

Result
Input : [ ][ ][tab]Hello,[ ]world!
Output: [ ][ ][ ][ ]Hello,[ ]world!

Input : [ ][ ][ ][tab][tab][ ][tab]Hello,[ ]world!
Output: [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]Hello,[ ]world!

Input : [tab]a[tab]ab[tab]abc[tab]Hello,[ ]world!
Output: [ ][ ][ ][ ]a[ ][ ][ ]ab[ ][ ]abc[ ]Hello,[ ]world!
 
Share this answer
 
v2
The C# version I published as Untabify and Tabify[^] acts only on leading whitespace.

But, according to the comments, the C version I started with dates back to 2-Jun-1998 -- you might guess from the date format that I was using OpenVMS extensively at the time (and I hadn't yet found out about ISO 8601). At that time, the company I worked for was becoming overrun by young whippersnappers who were using Visual Studio and refusing to set it to replace TABs with four SPACEs, which was the Company Standard. This was seriously irksome for we who were using plain-old-text-editors (EDT, VI, etc.) on OpenVMS. Imagine checking a file out of version control (CMS), opening it, and finding stuff all over the place!* There had to be a better way!

So I wrote a very simple untabify utility. Below is my str_untab function as I last touched it 2003-04-05.
is_null is basically an "IsNullOrEmpty" macro and str_fill appends a character to a string some number of times.

C++
# define __OVERRIDE__
# include "libstd.h"
# include "libstr.h"

char*
str_untab
(
    char*  subject1      ,
    char*  subject2      ,
    size_t chars_per_tab ,
    size_t max_chars
)
{
    size_t ch1       = 0 ;
    size_t ch2       = 0 ;
    size_t how_many      ;

    if ( !is_null ( subject1 ) && ( subject2 != NULL ) )
    {
        subject2[ch2] = '\0' ;

        while ( ( subject1[ch1] != '\0' ) && ( ch2 < max_chars ) )
        {
            if ( subject1[ch1] != '\t' )
            {
                subject2[ch2++] = subject1[ch1] ;
            }
            else
            {
                if ( chars_per_tab > 0 )
                {
                    how_many = chars_per_tab - ch2 % chars_per_tab ;

                    if ( max_chars - ch2 < how_many )
                    {
                        how_many = max_chars - ch2 ;
                    }

                    str_fill ( subject2 + ch2 , how_many , ' ' ) ;
                    ch2 += how_many ;
                }
            }

            ch1++ ;
        }

        subject2[ch2] = '\0' ;
    }

    return ( subject2 ) ;
}



* Unlike an actual VT100, the terminus emulator I use on Windows does not allow defining TAB stops.
 
Share this answer
 
v2
Here's PHP. You can run it in a terminal window (rather than a browser, which collapses whitespace), using:

php -f filename.php
PHP
<!--?php<!-- newline ---->$test1 = "  \tHello, world!";
$test2 = "   \t\t \tHello, world!";

echo "\nBEFORE... \n\n";
echo $test1 . "\n";
echo $test2 . "\n";

$test1New = str_replace("\t"," ",$test1);
$test2New = str_replace("\t"," ",$test2);

echo "AFTER... \n\n";
echo $test1New . "\n";
echo $test2New . "\n";

?>
 
Share this answer
 
JavaScript
function tab2space(str)
{
  var brk = str.match(/[^\t ]/).index;
  var wht = str.substr(0, brk);

  wht = wht.replace(/    /g, '\t').replace(/ {1,3}\t/g, '\t').replace(/\t/g, '    ');

  return(wht + str.substr(brk));
}
 
Share this answer
 
v2
ASM
section	.text
	global _start           ; gcc

_start:
    xor ecx, ecx            ; counter for spaces
    mov esi, msg            ; original string
main:
    mov eax, esi            ; get next pos

    cmp byte [eax], 0x20
    jne tab                 ; chekc for tab
    
    inc ecx                 ; increment space counter
    jmp end;
    
tab:
    cmp byte [eax], 0x09
    jne finish              ; finished lead part

    xor edx, edx            ; round up to next 4 boundary
    mov eax, ecx
    mov ebx, 0x04
    div ebx

    mov eax, 0x04
    sub eax, edx
    add ecx, eax
    
end:
	inc esi                 ; move to next pos
	jmp main

finish:
    test ecx, ecx
    jz tail
    
    push ecx
    ; print spaces
	mov	edx, 1      ; length
	mov	ecx, spc    ; letter
	mov	ebx, 1	    ; to stdout
	mov	eax, 4	    ; sys_write
	int	0x80        ; call kernel    
	
	pop ecx
	
    dec ecx
    jmp finish
    
tail:   
    mov eax, esi
    cmp byte [eax], 0x00
    jz quit
    ; print tail
    mov	edx, 1      ; length
	mov	ecx, eax    ; letter
	mov	ebx, 1	    ; to stdout
	mov	eax, 4	    ; sys_write
	int	0x80        ; call kernel    

    inc esi
    jmp tail
    
quit:
	mov	eax, 1	    ; sys_exit
	int	0x80        ; call kernel
    
section	.data
    msg	db	'  ', 0x09, 'Hello, world!', 0x00
    spc db  ' '


This code prints the result to the standard output using Linux kernel call...
To test: CodingGround - Compile and Execute Assembly Online[^]
 
Share this answer
 
Since it is more string handling, I'll simply append solution to previous challenge.

New code:

In 'reformat' routine:

C++
...
      while (curLocC != endLocC && (isTabOrWhiteSpace(curLocC))) {
         if (*curLocC == L'\t') {
            replaceTab();
            }
         else {
...

And new routine:
C++
void Reformatter::replaceTab() {
   //The logic before this occurs has found a tab at the curLocC position, so
   //don't need to do any other testing for that condition:
   //How many spaces do we need?  An easy way to find out is to rely upon the modulus operator,
   //so all tabs will line up on the same columns.

   const int tabColumns = 4;
   int curLen = resultC.length();

   int remainder = curLen%tabColumns;

   for (int x=0; x < tabColumns - remainder; ++x) {
      resultC.append(L" ");
      }
   }


Here is full code listing:
C++
#include <string>
#include <vector>
#include <tuple>
#include <ctime>
#include <iostream>
#include <iomanip>   //For cout 'setprecision'
#include "windows.h" //For QueryPerformanceCounter
using namespace std;
 
//I am going to use a tuple of: the input word, the small case output word, and the
//capped case output word, so they don't have to be calculated each time.
//So some defines are just for additional clarity:
#define GetTupleWord      get<0>
#define GetTupleSmallCase get<1>
#define GetTupleCapCase   get<2>

 
//Utility class for the timing...

//Windows high precision timer utility class for the timing.  Derived from 
//https://msdn.microsoft.com/en-us/library/windows/desktop/dn553408(v=vs.85).aspx
class TimerHighRes {
   private:
      LARGE_INTEGER startTimeC;
      LARGE_INTEGER endTimeC;
      LARGE_INTEGER elapsedMicrosecondsC;
      LARGE_INTEGER frequencyC;
      LARGE_INTEGER totalTimeC;  //Includes laps, but not pauses
      int numTimesC;

      void stop() {
         QueryPerformanceCounter(&endTimeC);
         elapsedMicrosecondsC.QuadPart = endTimeC.QuadPart - startTimeC.QuadPart;
         elapsedMicrosecondsC.QuadPart *= 1000000;
         elapsedMicrosecondsC.QuadPart /= frequencyC.QuadPart;
         totalTimeC.QuadPart += totalTimeC.QuadPart;
         numTimesC++;
         }

   public:
      TimerHighRes() : numTimesC(0) {
         totalTimeC.QuadPart = 0;
         QueryPerformanceFrequency(&frequencyC);
         }
 
      void start() {
         QueryPerformanceCounter(&startTimeC);
         }
 
      LARGE_INTEGER stopWithMicroseconds() {
         stop();
         return elapsedMicrosecondsC;
         }

      double stopWithMilliseconds() {
         stop();
         return (elapsedMicrosecondsC.QuadPart/(double)1000.0);
         }

      double milliseconds() {
         //This does not stop a timer - it only gives currently stored value.
         return (elapsedMicrosecondsC.QuadPart/(double)1000.0);
         }

      LARGE_INTEGER microseconds() {
         //This does not stop a timer - it only gives currently stored value.
         return elapsedMicrosecondsC;
         }

      double averageMilliseconds() {
         return (totalTimeC.QuadPart/(double)numTimesC);
         }

      void reset() {
         totalTimeC.QuadPart = 0;
         numTimesC = 0;
         elapsedMicrosecondsC.QuadPart = 0;
         startTimeC.QuadPart = 0;
         endTimeC.QuadPart = 0;
         }
   };
 

class Reformatter {
   private:
      //Good chance that the implied design requirements involve UNICODE text, but for quick
      //prototype of similar design use wstring:

      typedef tuple<wstring, wstring, wstring> WordTuples;
      vector<WordTuples> repeatingStringsC;     //This will address items like 'poopoopoop'
      vector<WordTuples> caseSensitiveSubsC;    //This means that the inputted word must be
                                                //matched on exact case.
      vector<WordTuples> nonCaseSensitiveSubsC; //By this, it is meant that the inputted word
                                                //can be either caps or small case in the text.
      vector<WordTuples> asteriskSubsC;         //The CapCase will be ignored - only the
                                                //asterisks in the small case will be modified.

      wstring resultC;
      size_t  strLenC;
      wstring::const_iterator beginLocC;
      wstring::const_iterator curLocC;
      wstring::const_iterator endLocC;
 
      bool replaceAsterisks();
      bool replaceNonCaseSensitiveBegin();
      bool replaceCaseSensitive();
      bool replaceRepeatingStrings();
      void replaceTab();
      bool previousWordIsCapitalized(wstring::const_iterator & thisWord, wstring::const_iterator & prevWord);
      bool nextWordIsCapitalized(wstring::const_iterator & thisWord, wstring::const_iterator & nextWord);
      void findBeginningOfPrevious(wstring::const_iterator & thisWord, wstring::const_iterator & prevWord);
      void findBeginningOfNext(wstring::const_iterator & thisWord, wstring::const_iterator & nextWord);
 
      bool isTabOrWhiteSpace(const wstring::const_iterator & temp);
 
   public:
      Reformatter() {
         repeatingStringsC.push_back(make_tuple(L"poo", L"p**", L"p"));
 
         caseSensitiveSubsC.push_back(make_tuple(L"PHB", L"boss", L"BOSS"));
 
         //The following 'gotten to be' must be defined before 'gotten':
         nonCaseSensitiveSubsC.push_back(make_tuple(L"gotten to be", L"become", L"BECOME"));
         nonCaseSensitiveSubsC.push_back(make_tuple(L"gotten", L"become", L"BECOME"));
 
         asteriskSubsC.push_back(make_tuple(L"nincompoop", L"nincompoop", L"nincomp##p"));
         asteriskSubsC.push_back(make_tuple(L"poop", L"p**p", L"P**P"));
         asteriskSubsC.push_back(make_tuple(L"p##p", L"p**p", L"P**P"));
         asteriskSubsC.push_back(make_tuple(L"ass", L"a**", L"A**"));
         }
 
      void reformat(const wstring & str);
      void outputResult();
   };
 

void Reformatter::outputResult() {
   wcout << L"OUTPUT: " << resultC << endl << endl;
   }
 

bool Reformatter::isTabOrWhiteSpace(const wstring::const_iterator & it) {
   if (*it == L'\t' || *it == L' ' || *it == L'.' || *it == L'!' || *it == L'_' || *it == L'\r' || *it == L'\n') return true;
   return false;
   }
 

void Reformatter::findBeginningOfNext(wstring::const_iterator & thisWord, wstring::const_iterator & nextWord) {
   //This is used in determining whether next word is shouted.
   //There is the possibility that there is no next word, but there is whitespace.
   //If that is the case, return prevWord = thisWord = curLocC.

   //Go to the whitespace at the end of the current word:
   while (thisWord != endLocC && !isTabOrWhiteSpace(thisWord)) ++thisWord;
   //Move 'thisWord' back to the beginning of the whitespace:
   nextWord = thisWord;
   //Now skip any additional whitespace:
   while (nextWord != endLocC && (isTabOrWhiteSpace(nextWord))) ++nextWord;
   if (nextWord == endLocC) {
      nextWord = endLocC;
      thisWord = endLocC;
      return;
      }
   }
 

void Reformatter::findBeginningOfPrevious(wstring::const_iterator & thisWord, wstring::const_iterator & prevWord) {
   //This is used in determining whether previous word is shouted.
   //There is the possibility that there is no previous word, but there is whitespace.
   //If that is the case, return prevWord = thisWord = (beginning of word).

   //Go to the whitespace before the current word:
   while (thisWord != beginLocC && !(isTabOrWhiteSpace(thisWord))) --thisWord;
   //Move 'thisWord' back to the beginning (one space forward), and set 'prevWord' current pos:
   prevWord = thisWord;
   ++thisWord;
   //Now skip any additional whitespace:
   while (prevWord != beginLocC && (isTabOrWhiteSpace(prevWord))) --prevWord;
   if (prevWord == beginLocC) {
      if (isTabOrWhiteSpace(prevWord)) prevWord = thisWord;
      return;
      }
   //We are now on the last character of the previous word.  Iterate to the beginning of it:
   while (prevWord != beginLocC && !(isTabOrWhiteSpace(prevWord))) --prevWord;
   //Check for the case where the user starts the input with a space character:
   if (isTabOrWhiteSpace(prevWord)) ++prevWord;
   }
 

bool Reformatter::previousWordIsCapitalized(wstring::const_iterator & thisWord,
               wstring::const_iterator & prevWord) {
 
   //We are working from the 'curLocC' position in the string.
   //Create a temporary iterator and find the beginning of the previous word.
   //If it reaches the beginning of the string, return 'true' so the 'shouting'
   //routines only rely on the following word.
   findBeginningOfPrevious(thisWord, prevWord);
   if (thisWord == prevWord) return true; //We will default to 'true' for the previous word
               //and 'false' for the next word, so next word processing can occur.
   //Now find the case of each letter until the next whitespace:
   while (!isTabOrWhiteSpace(prevWord)) {
      wchar_t temp = *prevWord;
      if (iswlower(*prevWord)) return false;
      ++prevWord;
      }
   return true;
   }
 

bool Reformatter::nextWordIsCapitalized(wstring::const_iterator & thisWord, wstring::const_iterator & nextWord) {
   //We are working from the 'curLocC' position in the string.
   //Create a temporary iterator and find the beginning of the previous word.
   //If it reaches the beginning of the string, return 'true' so the 'shouting'
   //routines only rely on the following word.
   findBeginningOfNext(thisWord, nextWord);
   if (thisWord == nextWord) return false;   //We are defaulting to 'true' for previous word
               //processing, and 'false' for this, so if there isn't any text before or
               //after the word, both processings will occur.
   //Now find the case of each letter until the next whitespace:
   while (nextWord != endLocC && !(isTabOrWhiteSpace(nextWord))) {
      wchar_t temp = *nextWord;
      if (iswlower(*nextWord)) return false;
      ++nextWord;
      }
   return true;
   }
 

bool Reformatter::replaceCaseSensitive() {
   //This returns true if a replacement has been made.
   bool found = false;
   wstring::const_iterator inputIterator;
   for (auto it = caseSensitiveSubsC.begin(); it != caseSensitiveSubsC.end(); ++it) {
      const wstring & str = GetTupleWord(*it);
      inputIterator = curLocC;
      for (int pos=0, len=str.length(); pos<len; ++pos) {
         found = true;
         if (inputIterator == endLocC || *inputIterator != str[pos]) {
            //The string doesn't match the criteria
            found = false;
            break;
            }
         ++inputIterator;
         }
      if (found) {
         //There is a non-specified scenario to take care of.  If the user inputs something
         //like 'PHBblog.com', in reality we may not want this to substitute 'boss'.  Here
         //is logic to take care of that scenario:
         if (inputIterator == endLocC || !(isTabOrWhiteSpace(inputIterator))) {
            return false;
            }
         //We now know that we need to replace the string, but we don't know if it needs to
         //be 'shouted'.
         //curLocC is still at the beginning of the text being checked against.  This can
         //only be at the beginning of a word.
         wstring::const_iterator thisWord = curLocC;
         wstring::const_iterator prevWord = curLocC;
         wstring::const_iterator nextWord = curLocC;
         //If someone has different length words for cap and small, this will need to be changed in the cases:
         if (previousWordIsCapitalized(thisWord, prevWord) && nextWordIsCapitalized(thisWord, nextWord)) {
            resultC.append(GetTupleCapCase(*it));
            }
         else {
            resultC.append(GetTupleSmallCase(*it));
            }
         //Now append any whitespace after the string:
         curLocC += str.length();
         while (curLocC != endLocC && (isTabOrWhiteSpace(curLocC))) {
            resultC.append(1, *curLocC);
            ++curLocC;
            }
         return found;
         }
      }
   return found;
   }
 

bool Reformatter::replaceNonCaseSensitiveBegin() {
   //By 'non-case-sensitive', it is meant that the routine will check for either capped or
   //non-capped matches.
   bool found;
   vector<tuple<wstring, wstring, wstring>>::iterator it;
   wstring::const_iterator inputIterator;
   const wstring * str;
 
   //If there isn't a character before 'curLocC' we don't need to look any further.
   if (curLocC == beginLocC) return false;
   if (!(curLocC-1==beginLocC || isTabOrWhiteSpace(curLocC-1))) return false;
   if (!isTabOrWhiteSpace(curLocC-1)) return false;
 
   for (it = nonCaseSensitiveSubsC.begin(); it != nonCaseSensitiveSubsC.end(); ++it) {
      str = &GetTupleWord(*it);
      inputIterator = curLocC;
      for (int pos=0, len=str->length(); pos<len; ++pos) {
         found = true;
         if (inputIterator == endLocC || towlower(*inputIterator) != (*str)[pos]) {
            found = false;
            break;
            }
         ++inputIterator;
         }
      if (found) break;
      }
   if (found) {
      //We need to see if it is capped, and if so, cap the output:
      inputIterator = curLocC;
      bool isCapped = true;
      for (int pos=0, len=(*str).length(); pos<len; ++pos) {
         if (iswlower(*inputIterator)) {
            isCapped = false;
            break;
            }
         ++inputIterator;
         }
      if (isCapped) {
         resultC.append(GetTupleCapCase(*it));
         }
      else {
         resultC.append(GetTupleSmallCase(*it));
         }
      curLocC += GetTupleWord(*it).length();
      return true;
      }
   return false;
   }
 

bool Reformatter::replaceAsterisks() {
   bool found;
   vector<tuple<wstring, wstring, wstring>>::iterator it;
   wstring::const_iterator inputIterator;
   const wstring * str;
 
   for (it = asteriskSubsC.begin(); it != asteriskSubsC.end(); ++it) {
      str = &GetTupleWord(*it);
      inputIterator = curLocC;
      for (int pos=0, len=str->length(); pos<len; ++pos) {
         found = true;
         if (inputIterator == endLocC || towlower(*inputIterator) != (*str)[pos]) {
            found = false;
            break;
            }
         ++inputIterator;
         }
      if (found) break;
      }
   if (found) {
      //Go through the small cap tuple and if a character is an asterisk, replace
      //the corresponding character with an asterisk:
      str = &GetTupleSmallCase(*it);
      inputIterator = curLocC;
      for (int pos=0, len=(*str).length(); pos<len; ++pos) {
         if ((*str)[pos] == L'*') {
            resultC.append(L"*");
            }
         else {
            resultC.append(1, *curLocC);
            }
         ++curLocC;
         ++inputIterator;
         }
      return true;
      }
   return false;
   }
 

bool Reformatter::replaceRepeatingStrings() {
   bool found;
   vector<tuple<wstring, wstring, wstring>>::iterator it;
   wstring::const_iterator inputIterator;
   const wstring * str;
 
   for (it = repeatingStringsC.begin(); it != repeatingStringsC.end(); ++it) {
      str = &GetTupleWord(*it);
      inputIterator = curLocC;
      for (int pos=0, len=str->length(); pos<len; ++pos) {
         found = true;
         if (inputIterator == endLocC || towlower(*inputIterator) != (*str)[pos]) {
            found = false;
            break;
            }
         ++inputIterator;
         }
      if (found) {
         //The inputIterator is pointing to the letter right after the initial string
         //(ie, the final 'p' in 'poop')
         str = &GetTupleCapCase(*it);
         if (*inputIterator != (*str)[0]) found = false;
         }
      if (found) break;
      }
   if (found) {
      //Go through the small cap tuple and if a character is an asterisk, replace
      //the corresponding character with an asterisk:
      str = &GetTupleSmallCase(*it);
      inputIterator = curLocC;
      for (int pos=0, len=(*str).length(); pos<len; ++pos) {
         if ((*str)[pos] == L'*') {
            resultC.append(L"*");
            }
         else {
            resultC.append(1, *curLocC);
            }
         ++curLocC;
         ++inputIterator;
         }
      return true;
      }
   return false;
   }
 

void Reformatter::replaceTab() {
   //The logic before this occurs has found a tab at the curLocC position, so
   //don't need to do any other testing for that condition:
   //How many spaces do we need?  An easy way to find out is to rely upon the modulus operator,
   //so all tabs will line up on the same columns.

   const int tabColumns = 4;
   int curLen = resultC.length();

   int remainder = curLen%tabColumns;

   for (int x=0; x < tabColumns - remainder; ++x) {
      resultC.append(L" ");
      }
   }
 

void Reformatter::reformat(const wstring & str) {
   resultC.reserve(str.length()+1);
   resultC.clear();
   beginLocC = str.begin();
   curLocC = beginLocC;
   strLenC = str.length();
   endLocC = str.end();
   while (curLocC != endLocC) {
      //Skip processing on whitespace:
      while (curLocC != endLocC && (isTabOrWhiteSpace(curLocC))) {
         if (*curLocC == L'\t') {
            replaceTab();
            }
         else {
            resultC.append(1, *curLocC);
            }
         ++curLocC;
         }
      if (curLocC == endLocC) return;
      //Now do the non-whitespace processing:
      if (!(replaceCaseSensitive() || replaceNonCaseSensitiveBegin() ||
                  replaceRepeatingStrings() || replaceAsterisks())) {
         resultC.append(1, *curLocC);
         ++curLocC;
         }
      }
   }
 

int main() {
   static Reformatter reformatter;
 
   //First, some tests to show that it is working:
   cout << "[ ][ ][tab]Hello, world!" << endl;
   cout << "        1234123412341234123412341234123412341234123412341234123412341243123412341234" << endl;
   wstring str = L"  \tHello, world!";
   reformatter.reformat(str);
   wcout << L"INPUT : " << str << endl;
   reformatter.outputResult();

   cout << "[ ][ ][ ][tab][tab][ ][tab]Hello, world!" << endl;
   cout << "        1234123412341234123412341234123412341234123412341234123412341243123412341234" << endl;
   str = L"   \t\t \tHello, world!";
   reformatter.reformat(str);
   wcout << L"INPUT : " << str << endl;
   reformatter.outputResult();

   cout << "[ ][ ][ ][ ][ ][tab][tab][ ][tab]Hello, world!" << endl;
   cout << "        1234123412341234123412341234123412341234123412341234123412341243123412341234" << endl;
   str = L"     \t\t \tHello, world!";
   reformatter.reformat(str);
   wcout << L"INPUT : " << str << endl;
   reformatter.outputResult();
 
   //Changed the following to 'phb.com', so that program checks for more general case.
   //phb.com redirects to another site in reality, but there is no reason not to check
   //against being a URL in the implied design requirements.
   cout << "I will insert tabs into phrase for previous challenge testing.  Note that 'boss' becomes capped or non-capped depending upon surrounding words."
               << endl << "Also, 'PHB' becomes 'boss' depending upon whether 'PHB' is by itself."
               << endl;
   str = L"MY\tPHB \tHAS started his new BLOG\tPHB_BLOG.com.\t\tAnd PHBBlog.com! \t He's gotten TO BE "
                     " SUCH A MISBEGOTTEN\tPOOPHEAD!";
   reformatter.reformat(str);
   wcout << L"INPUT : " << str << endl;
   reformatter.outputResult();
 
   //Now do an timing run:
   TimerHighRes timerHighRes;
   LARGE_INTEGER timeRighRes;
   vector<float> timeList;
   int numIterations = 100;
   int totalTests = 10;
   for (int numTimes=0; numTimes<totalTests; ++numTimes) {
      timerHighRes.start();
      for (int x=0; x<numIterations; ++x) {
         reformatter.reformat(L"MY\tPHB \tHAS started his new BLOG\tPHB_BLOG.com.\t\tAnd PHBBlog.com! \t He's gotten TO BE "
                     " SUCH A MISBEGOTTEN\tPOOPHEAD!");
         //reformatter.reformat(L"My \tPHB is such a\tpoophead!\tIt's gotten worse since his\tpromotion!");
         //reformatter.reformat(L"My PHB has started his new blog phblog.com. He's SUCH A MISBEGOTTEN POOPHEAD!");
         //reformatter.reformat(L"I GOTTEN PHB lettuce!");
         //reformatter.reformat(L"Not Poopoop, but PoopPoop!!");  //Note that this will handle subsequent 'poops' correctly if 2 'p's together at end.
         }
      timeRighRes = timerHighRes.stopWithMicroseconds();
      cout << "Time: " << setprecision(9) <<  timeRighRes.QuadPart << " microseconds" << endl;
      timeList.push_back(timeRighRes.QuadPart/(float)1000);
      }
   float avg = 0.0;
   for (int i=0; i<totalTests; ++i) avg += timeList[i];
   avg = avg/totalTests;
   cout << endl << "Average = " << avg << " milliseconds";
   cout << endl << "Enter any key, then an 'Enter' to exit: ";
   cin >> timeRighRes.QuadPart; //Junk way to exit console, but good enough!
   return 0;
   }
 
Share this answer
 
Ahhhh! I thought this would be posted in the lounge like last time. Glad I caught it! Decided to go with something unique that hasn't been proposed yet =D

Might even prove to be useful out-of-the-box if you want to replace tabs in source files in a project directory. I prefer tabs but to each their own.

::Replace Tabs in matched files in a directory (optional sub-directories)

@ECHO OFF
SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
SET localDir=%~dp0%
SET /A spaceNumber=4

:EnterDirectory
SET /P workingDir=Enter working directory (blank to use current): 
IF "!workingDir!" EQU "" SET workingDir=%localDir%
IF NOT EXIST "!workingDir!" (
  ECHO Invalid directory.
  SET workingDir=
  GOTO :EnterDirectory
)
SET /P recursive=Include subdirectories (Y/N)? 

ECHO.
ECHO Enter files to convert. Wildcards supported.
ECHO Separate files with a comma (ex. *.txt, blah.dat)
SET /P files=Enter files: 

IF /I "%recursive%" EQU "Y" (
  FOR /R "!workingDir!" %%G IN (%files%) DO (
    REN "%%G" "%%~nxG.replacebackup"
    (MORE /t%spaceNumber% < "%%G.replacebackup") > "%%G"
  )
) ELSE (
  PUSHD "!workingDir!"
  FOR %%G IN (%files%) DO (
    REN "%%G" "%%G.replacebackup"
    (MORE /t%spaceNumber% < "%%G.replacebackup") > "%%G"
  )
  POPD
)
ECHO Replacement complete.
ECHO.
SET /P removeBackup=Remove backup files (*.replacebackup)? 
IF /I "%removeBackup%" EQU "Y" (
  PUSHD "!workingDir!"
  DEL /P /S *.replacebackup
  POPD
  ECHO Deletion complete.
)

ECHO Operations complete.  Press any key to exit.
ENDLOCAL
PAUSE > NUL


To change the number of spaces to replace every tab with simply change SET /A spaceNumber=4 to whatever number of spaces you want. Also supports filename spaces with the ? wildcard (ex. filename?with?spaces.txt)
 
Share this answer
 
v5
Haskell. Just gets the job done.

untab tabSize s = foldl processChar "" s
  where
    processChar s '\t' = s ++ (replicate (spaceCount s) ' ')
    processChar s c = s ++ [c]
    spaceCount s = tabSize - ((length s) `mod` tabSize)


with a test harness of

main =
  do
    let s = "  \tHello, world!"
    check 4 "  \tHello, world!"
    check 4 "   \t\t \tHello, world!"
    check 3 "  \tHello, world!"
    check 3 "   \t\t \tHello, world!"
    check 8 "  \tHello, world!"
    check 8 "   \t\t \tHello, world!"
  where
    check t s =
      do
        putStrLn ("Before: " ++ (printWhiteSpace s))
        putStrLn ("After : " ++ (printWhiteSpace $ untab t s) ++ "     (tab-size = " ++ (show t) ++ ")")


produces this output:

Before: [ ][ ]\tHello,[ ]world!
After : [ ][ ][ ][ ]Hello,[ ]world!     (tab-size = 4)
Before: [ ][ ][ ]\t\t[ ]\tHello,[ ]world!
After : [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]Hello,[ ]world!     (tab-size = 4)
Before: [ ][ ]\tHello,[ ]world!
After : [ ][ ][ ]Hello,[ ]world!     (tab-size = 3)
Before: [ ][ ][ ]\t\t[ ]\tHello,[ ]world!
After : [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]Hello,[ ]world!     (tab-size = 3)
Before: [ ][ ]\tHello,[ ]world!
After : [ ][ ][ ][ ][ ][ ][ ][ ]Hello,[ ]world!     (tab-size = 8)
Before: [ ][ ][ ]\t\t[ ]\tHello,[ ]world!
After : [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]Hello,[ ]world!     (tab-size = 8)
 
Share this answer
 
TAB EXPANSION USING VISUAL PROLOG 7.5

This approach uses a stream to collect the text, avoiding creation of multiple intermediate strings or managing your own binary buffer.

class predicates
    expandTabs : ( string Input, positive TabSize ) -> string ExpandedString.
clauses
    expandTabs( S, 0 ) = S :- !.
    expandTabs( S, TabSize ) = ExpandedString :-
        OS = outputStream_string::new(),
        StrX0 = string::length(S) - 1,
        foreach X0 = std::fromTo(0,StrX0) do
            C = subChar(S,X0),
            if C = '\t' then
                    advanceTab( OS, TabSize, ' ' )
                else
                    OS:write(C)
            end if
        end foreach,
        ExpandedString = OS:getString().


        class predicates
            advanceTab : ( outputStream_string, positive TabSize, char FillChar ).
        clauses
            advanceTab( OS, TabSize, FillChar ):-
                OnTabColumn = { () :-
                                OS:getPosition() mod (2*TabSize) = 0
                                },
                if OnTabColumn() then
                        % emit a full tab if on a tab column
                        foreach _ = std::fromTo(1,TabSize) do
                            OS:write(FillChar)
                        end foreach, !
                    else
                        % emit tab characters until reach next tab column
                        std::repeat(),
                            OS:write(FillChar),
                            OnTabColumn(), !
                end if.
            advanceTab( _,_,_ ):-
                error( predicate_fullname(), 
                    "Should never get here but needed to make advanceTab/3 a procedure.").



A test/0 predicate is below. Note the use of anomymous predicates Prepare, Restore and ShowMe to simplify the code.

C#
    open core, string, list, outputstream_string, vpiCommonDialogs

clauses
test():-
    S1 = "[ ][ ][tab]Hello, world!",
    S2 = "[ ][ ][ ][ ]Hello, World!",
    S3 = "[ ][ ][ ][tab][tab][ ][tab]Hello, world!",
    Prepare = { (S) = TestStr :-
        TestStr = replaceAll(
            replaceAll(S,"[ ]"," "),
            "[tab]", "\t" )
        },
    Restore = { (S) = RestoredStr :-
        RestoredStr = replaceAll(
            replaceAll(S," ","[ ]"),
            "\t","[tab]")
        },
    ShowMe = { (SS) :-
        foreach M = list::getMember_nd( SS ) do
            PreppedStr = Prepare( M ),
            DeTabStr = expandTabs(PreppedStr,4),
            RestoredStr = Restore( DeTabStr ),
            stdio::write("\n\n",M, "<== trial string in display mode",
                "\n", PreppedStr, "<== string to test",
                "\n", DeTabStr, "<== expanded tab string",
                "\n", RestoredStr, "<== expanded tab string in display mode" )
        end foreach
        },
    ShowMe( [S1, S2, S3] ).


Output of the test/0 predicate is below:

TEST OUTPUT:
[ ]  ][tab]Hello, world!<== trial string in display mode
  	Hello, world!<== string to test
    Hello, world!<== expanded tab string
[ ]  ][ ][ ]Hello,[ ]world!<== expanded tab string in display mode

[ ]  ][ ][ ]Hello, World!<== trial string in display mode
    Hello, World!<== string to test
    Hello, World!<== expanded tab string
[ ]  ][ ][ ]Hello,[ ]World!<== expanded tab string in display mode

[ ]  ][ ][tab][tab][ ][tab]Hello, world!<== trial string in display mode
   		 	Hello, world!<== string to test
            Hello, world!<== expanded tab string
[ ]  ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]Hello,[ ]world!<== expanded tab string in display mode
 
Share this answer
 
Here's my solution with C#. Clear, elegant, easy to maintain and... very boring :)

C#
string inStr = "   \t\t \tHello, world!";
string prefixStr;
string suffixStr;
const int TAB_POS = 4; // TAB_POS > 0
int tabPos;

// When a tab is found add 1 - TAB_POS filler chars
tabPos = inStr.IndexOf('\t');
while (tabPos >= 0)
{
    // Replace tab with TAB_POS - (tabPos % TAB_POS) filler chars
    prefixStr = inStr.Substring(0, tabPos); // Characters before tab char
    suffixStr = inStr.Substring(tabPos + 1, inStr.Length - (tabPos + 1)); // Characters after tab char
    // Concat prefix, filler and suffix
    inStr = prefixStr + new string(' ', TAB_POS - (tabPos % TAB_POS)) + suffixStr;
    // Next tab
    tabPos = inStr.IndexOf('\t');
}
// Output 'expanded' inStr
 
Share this answer
 
Using Phix

function expand_tabs(string text, integer tabwidth=4)
    while 1 do
        integer tab = find('\t',text)
        if tab=0 then exit end if
        text[tab..tab] = repeat(' ',tabwidth-remainder(tab-1,tabwidth))
    end while
    return text&'\n'
end function

Test program (input/output same as Fraser Duthie's Javascript entry)

puts(1,"Using tab positions 4 characters apart:\n")
puts(1,expand_tabs("\tHello World",4));
puts(1,expand_tabs(" \tHello World",4));
puts(1,expand_tabs("  \tHello World",4));
puts(1,expand_tabs("   \tHello World",4));
puts(1,expand_tabs("    \tHello World",4));
puts(1,expand_tabs("     \tHello World",4));
puts(1,expand_tabs("      \tHello World",4));
puts(1,expand_tabs("       \tHello World",4));
puts(1,"Using tab positions 6 characters apart:\n")
puts(1,expand_tabs("\tHello World",6));
puts(1,expand_tabs(" \tHello World",6));
puts(1,expand_tabs("  \tHello World",6));
puts(1,expand_tabs("   \tHello World",6));
puts(1,expand_tabs("    \tHello World",6));
puts(1,expand_tabs("     \tHello World",6));
puts(1,expand_tabs("      \tHello World",6));
puts(1,expand_tabs("       \tHello World",6));
puts(1,"Using tab positions 9 characters apart:\n")
puts(1,expand_tabs("\tHello World",9));
puts(1,expand_tabs(" \tHello World",9));
puts(1,expand_tabs("  \tHello World",9));
puts(1,expand_tabs("   \tHello World",9));
puts(1,expand_tabs("    \tHello World",9));
puts(1,expand_tabs("     \tHello World",9));
puts(1,expand_tabs("      \tHello World",9));
puts(1,expand_tabs("       \tHello World",9));
 
Share this answer
 
v2
Here's one in C++:
C++
#include <string>

std::string tabs_to_spaces(const std::string& input, size_t fill_width)
{
    std::string output;
    size_t curr_pos = 0; // current position in input string
    size_t tab_pos;      // positions of next tab in input string

    // find next tab in input string
    while( (tab_pos = input.find('\t', curr_pos))  != std::string::npos ) {

        // copy input to output up to next tab
        // We can skip this call if the next char in the input string is a tab
        if(tab_pos - curr_pos > 0)
            output.append(input, curr_pos, tab_pos - curr_pos);

        // Number of spaces to add
        // output.length() mod fill_width tells us how many chars past last tab
        // stop we are, so subtract that value from 
        size_t len = fill_width - (output.length() % fill_width);
        output.append(len, ' ');

        //new current position is one past the tab char just processed
        curr_pos = tab_pos + 1;
    }

    // we're now one char past the last tab in the input string,
    // so add any remaning portion of input string to output
    if(input.length() > curr_pos)
        output.append(input, curr_pos, std::string::npos);

    return output;
}
</string>
 
Share this answer
 
I was going for a one liner in C++

C++
void tabit(char* s)
{
  char w[] = " ";
  for (int i = 0, c = 0, a = 0; s[i] != '\0'; cout << ((s[i] == '\t') ? ((a = (4 - (c % 4))) == 0 ? "" : (a == 1) ^ !(c += a) ? " " : a == 2 ? "  " : a == 3 ? "   " : "    ") : (((w[0] = s[i]) ^ c++) == 0 ? "" : w)), i++);
	
  cout << endl;
}


The driver
C++
int main()
{
	cout << "12345678901234567890123456789\n";
	cout << "|   |   |   |   |   |   |   |\n";
	tabit(" \tHello, World!");
	cout << "|   |   |   |   |   |   |   |\n";	
	tabit("   \t\t \tHello, World!");
}


Output
12345678901234567890123456789
|   |   |   |   |   |   |   |
    Hello, World!
|   |   |   |   |   |   |   |
            Hello, World!
 
Share this answer
 
Short n sweet... (Can be expanded for readability - but that's not the point here!)
C# solution:
C#
private string RemoveTabs(string input)
{
	var prefix = new Regex("^[\t ]*").Match(input).Value;
	return new string(' ', SpaceCountFromPrefix(prefix)) + input.TrimStart('\t', ' ');
}
private int SpaceCountFromPrefix(string prefix)
{
	var spaces = prefix.Split('\t').Select(s => s.Length).ToArray();
	return spaces.Take(spaces.Length - 1).Select(i => (i / 4) * 4 + 4).Sum()
		   + spaces.Last();
}
 
Share this answer
 

Page 2 of 2
1 2

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900