Search code examples
replacestring-matchinglogo-lang

How to perform sub-string substitutions in LOGO?


I'd like to get string from user, parse it, then run the parsed commands.

The string input will be something like "F20N20E10L10", guaranteed no spaces.

This input I want to convert to LOGO commands with substitutions like these:

  • "F" → fd
  • "N" → seth 0 fd
  • "E" → seth 90 fd
  • "L" → lt 90 fd

So the string input above would be converted to these LOGO commands:

fd 20 seth 0 fd 20 seth 90 fd 10 lt 90 fd 10

All Forth dialects allow input, and interpreting a string of commands.

But I can't find any with search and replace string operations. Is this possible in any dialect of LOGO? Willing to consider any.

Thank you for reading.


Solution

  • It's been a while since I've written any Logo, so I'm not sure if this is the easiest way, but here's one way you can do it. The general idea is you can work with strings as lists of characters, using FIRST, LAST, BUTFIRST, and BUTLAST to get at different parts of the string. (I tested this on the first two online Logo interpreters I could find -- http://www.logointerpreter.com/turtle-editor.php and http://www.calormen.com/jslogo/ -- and it ran fine on both, but you might need some small changes for other Logo dialects.)

    TO RUN_COMMANDS :commands
      IF (EMPTY? :commands) [STOP]
    
      MAKE "first_command (FIRST :commands)
      MAKE "rest_of_commands (BUTFIRST :commands)
    
      IF (NOT EMPTY? :rest_of_commands) [MAKE "split (GET_NUMBER :rest_of_commands ")]
      MAKE "numeric_argument (LAST :split)
      MAKE "rest_of_commands (FIRST :split)
    
      RUN_SINGLE_COMMAND :first_command :numeric_argument
    
      RUN_COMMANDS :rest_of_commands
    END
    
    TO MERGE_STRING :word :characters
      IF (NOT EMPTY? :characters) [OP (MERGE_STRING (WORD :word (FIRST :characters)) (BUTFIRST :characters))]
      OP :WORD
    END
    
    TO GET_NUMBER :word :number
      IF (AND (NOT (EMPTY? :word)) (IS_DIGIT (FIRST :word))) [OP (SE (GET_NUMBER (BUTFIRST :word) (LPUT (FIRST :word) :number))]
      OP (SE (MERGE_STRING " :word) (MERGE_STRING " :number))
    END
    
    TO IS_DIGIT :character
      OP (OR
      :character = "0
      :character = "1
      :character = "2
      :character = "3
      :character = "4
      :character = "5
      :character = "6
      :character = "7
      :character = "8
      :character = "9)
    END
    
    TO RUN_SINGLE_COMMAND :command :parameter
      (PRINT_COMMAND :command :parameter)
      IF (:command = "F) [FD :parameter]
      IF (:command = "B) [BK :parameter]
      IF (:command = "L) [LT 90 FD :parameter]
      IF (:command = "R) [RT 90 FD :parameter]
      IF (:command = "N) [SETH 0 FD :parameter]
      IF (:command = "S) [SETH 180 FD :parameter]
      IF (:command = "E) [SETH 90 FD :parameter]
      IF (:command = "W) [SETH 270 FD :parameter]
    END
    
    TO PRINT_COMMAND :command :parameter
      IF (:command = "F) [PRINT (SE "FD :parameter)]
      IF (:command = "B) [PRINT (SE "BK :parameter)]
      IF (:command = "L) [PRINT (SE "LT 90 "FD :parameter)]
      IF (:command = "R) [PRINT (SE "RT 90 "FD :parameter)]
      IF (:command = "N) [PRINT (SE "SETH 0 "FD :parameter)]
      IF (:command = "S) [PRINT (SE "SETH 180 "FD :parameter)]
      IF (:command = "E) [PRINT (SE "SETH 90 "FD :parameter)]
      IF (:command = "W) [PRINT (SE "SETH 270 "FD :parameter)]
    END
    

    Then, try running:

    RUN_COMMANDS "F20N20E10L10
    

    This prints and executes the following:

    FD 20
    SETH 0 FD 20
    SETH 90 FD 10
    LT 90 FD 10
    

    Some Explanation

    RUN_COMMANDS is the main function. It:

    1. Extracts the first letter from the sting (I'm assuming each command is abbreviated as a single letter)
    2. Calls GET_NUMBER which extracts a number (which could be multiple characters) from the start of the string.
    3. Passes the single-letter abbreviated command and number to RUN_SINGLE_COMMAND
    4. Recurses to repeat the process

    IS_DIGIT is used within GET_NUMBER to check if a character is numeric (although I would bet some Logo dialects have a built-in function for this.)

    MERGE_STRING is used because I had some multi-character Words ("word" is Logo-speak for a string) which I had turned into Lists of single-character Words, and I wanted to merge the list back into a single Word. This might not actually be necessary, though.

    RUN_SINGLE_COMMAND executes each individual command that was parsed from the input string. I just used a big IF statement, rather than using a function which interprets the string as code as you suggested. (Some Logo dialects might have such a function, but I'm not sure of a standard one.) RUN_SINGLE_COMMAND also calls PRINT_COMMAND, which prints out the unabbreviated command as it's run.


    Potential Stack Overflows

    I used lots of recursion because it's more idiomatic of Logo, and because Logo dialects often don't have a lot of looping constructs (other than REPEAT). But I did it in a careless way, since I was just writing this quickly to give you the general idea. In particular, I didn't worry about stack overflows (no pun intended), which I think could occur if you provided a long input string. To deal with this, you should make sure any recursive path that can be called arbitrarily many times is a tail call, which Logo will optimize away.

    At a glance, it looks like RUN_COMMANDS is fine but MERGE_STRING and GET_NUMBER are reversed -- instead of IF <condition> [<recursive_call>] followed by OUTPUT <return_value>, it would be better to do IF (NOT <condition>) [OUTPUT <return_value>] followed by <recursive_call>. Testing for stack overflows and applying this fix I have left as an exercise for the reader. :)