15
\$\begingroup\$

The first programming language I was exposed to was Sinclair BASIC. Like many BASIC dialects, it requires all source code lines to be numbered.

As a result, use of the GO TO command was idiomatic and jumps execution to the given line number (no labels).

Also there is a related GO SUB command which can be used as a rudimentary function call. Again, execution jumps to the given line number, but when a RETURN command is reached, execution jumps back to the next instruction after the GO SUB.

Similarly the RUN command will restart program execution at the given line.

Anyone who has spent any time in a line-numbered BASIC interpreter will have learned to use a numbering scheme with gaps in it. This is so that it is easier to insert new lines of code. However even then, you may still find yourself needing to insert new lines in between consecutively numbered lines.


Given a line-numbered BASIC listing as input, output the same program but renumbered such that the line numbers start at 10 and increment by steps of 10. The input listing may have GO TO or GO SUB commands, so the numbers associated with these must also be adjusted.

  • GO TO and GO SUB commands are either on their own lines or at the end of IF THEN lines. Its safe to say ^(\d+) .*GO (TO|SUB) (\d+)$ is sufficient to match such lines. These commands in quotes should be ignored.

  • RUN commands will always be on their own lines. In this case a line number is optional. If it is missing, then the interpreter simply starts at the top of the program.

  • If a GO TO, GO SUB or RUN command references a non-existent line, then it will instead jump to the next defined line. Your entry needs to deal with this and ensure any such line references are fixed so they point to the correct line. Behaviour may be undefined if a line number after the end of the program is given in one of these commands.

  • Line numbers will always be positive integers 1 to 9999 (as per the manual). This means that input programs will never have more than 999 lines.

  • Input lines will always be numbered in numerically ascending order.

  • For the purposes of this challenge, input listings will only contain printable ASCII. You don't need to worry about the ZX character set. Having said that, if your entry is actually written in ZX BASIC or appropriate z80 assembly/machine code (and there are emulators out there), then you may choose for your input to be encoded in the ZX character set instead.

  • You may not use any renumber libraries or utilities that are specifically tailored for this purpose.

Example Input:

1 REM "A rearranged guessing game"
2 INPUT A: CLS
3 INPUT "Guess the number ", B
10 IF A=B THEN PRINT "Correct": STOP
100 IF A<B THEN GO SUB 125
120 IF A>B THEN GO SUB 122
121 GO TO 3
125 PRINT "Try again"
126 RETURN
127 REM "An example of GO TO 7 and GO SUB 13 in quotes"

Example Output:

10 REM "A rearranged guessing game"
20 INPUT A: CLS
30 INPUT "Guess the number ", B
40 IF A=B THEN PRINT "Correct": STOP
50 IF A<B THEN GO SUB 80
60 IF A>B THEN GO SUB 80
70 GO TO 30
80 PRINT "Try again"
90 RETURN
100 REM "An example of GO TO 7 and GO SUB 13 in quotes"

I wanted to link to a ZX BASIC manual. The best I could find seems to be http://www.worldofspectrum.org/ZXBasicManual/index.html but this seems to be a dead link. The wayback machine has a copy though.

\$\endgroup\$
12
  • 7
    \$\begingroup\$ Also conrgats on asking the 5000th question! \$\endgroup\$ Feb 19 '16 at 17:43
  • 1
    \$\begingroup\$ Nostalgia time - my first PC was a Spectrum 48K and one of my first assembly programs was a renumberer \$\endgroup\$
    – edc65
    Feb 19 '16 at 17:53
  • 2
    \$\begingroup\$ @edc65 Do you still have your renumber assembly code? If so you'd be very welcome to post it as an answer! \$\endgroup\$ Feb 19 '16 at 19:21
  • 1
    \$\begingroup\$ The test case should include at least one goto/gosub in a string literal. \$\endgroup\$ Feb 20 '16 at 9:22
  • 1
    \$\begingroup\$ I found a mention: the ZX81 allows computed GOTOs and GOSUBs as in GOTO 100 + A*10, and Appendix C of the ZX Spectrum Manual lists GO TO as accepting a numerical expression (no restriction to constants). Here's a discussion of the merits of computed GOTO on ZX80 and ZX81. BTW, I've no idea why the space was added in the Spectrum version. \$\endgroup\$ Sep 10 '18 at 16:02
5
\$\begingroup\$

JavaScript (ES6) 177

Edit Added the (costly) scan for the next valid line number

l=>l.split`
`.map((x,i)=>([,n,t]=x.match(/(\d+)(.*)/),l[n]=10*-~i,t),l=[]).map((x,i)=>10*-~i+x.replace(/(UN |GO TO |UB )(\d+)$/,(a,b,c)=>(l.some((v,i)=>i<c?0:a=b+v),a))).join`
`

TEST

f=l=>
  l.split`\n`
  .map((x,i)=>([,n,t]=x.match(/(\d+)(.*)/),l[n]=10*-~i,t),l=[])
  .map((x,i)=>10*-~i+x.replace(/(UN |GO TO |UB )(\d+)$/,(a,b,c)=>(l.some((v,i)=>i<c?0:a=b+v),a)))
  .join`\n`
        
//TEST
console.log=x=>O.textContent+=x+'\n'
  
test=`1 REM "A rearranged guessing game"
2 INPUT A: CLS
3 INPUT "Guess the number ", B
10 IF A=B THEN PRINT "Correct": STOP
100 IF A<B THEN GO SUB 125
120 IF A>B THEN GO SUB 122
121 GO TO 3
125 PRINT "Try again"
126 RETURN`
console.log(test+'\n\n'+f(test))
<pre id=O></pre>

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Looks good. My +1 stands :) \$\endgroup\$ Feb 19 '16 at 21:57
2
\$\begingroup\$

Perl 6, 147 145 144 142 bytes

{my%a;.trans(/^^(\d+)/=>{%a{$0}=$+=10}).trans(/:s<!after \"\N*>(UN |GO TO |UB )(\d+)<!before \N*\">/=>{$0~%a{%a.keys».Num.grep(*>=$1).min}})}

This can probably be golfed down a bit more.

Expanded

my &f = -> $s { 
    my %line-map; # This will map the old line numbers to the new ones

    $s.trans(/^^(\d+)/                    # This .trans creates the line number map
             => { %line-map{$0} = $+=10 } # as well as replaces the actual line numbers
            )\
      # This .trans replaces all the line numbers for each GO TO, GO SUB, RUN
      .trans(/:s<!after \"\N*>(UN |GO TO |UB )(\d+)<!before \N*\">/ 
             => {$0 ~ %line-map{%line-map.keys».Num.grep(*>=$1).min} } 
            )
}
\$\endgroup\$
1
  • \$\begingroup\$ no reason to use the method .min. use {min %line-map.keys».Num.grep:*>=$1 instead \$\endgroup\$
    – Ven
    Apr 24 '16 at 11:11
1
\$\begingroup\$

Visual Basic for Applications, 288 bytes

I couldn't resist giving a solution in a BASIC dialect. Probably works with Visual Basic 6/.NET or other modern variants with minor changes.

Sub n(t,a)
f=Chr(10)
u=Chr(0)
Open t For Input As 1
a=f &Input(LOF(1),1)&f
Close
j=10
For i=1 To 9999
q=f &j &u
g=" GO TO "
w=i &f
m=j &f
a=Replace(Replace(Replace(Replace(a,g &w,g &m),f &i &" ",q),"B "&w,"B "&m),"UN "&w,"UN "&m)
If InStr(1,a,q)Then j=j+10
Next
a=Replace(a,u," ")
End Sub

I used a lot of one-letter variables for conciseness. Also, I supressed all unnecessary whitespaces (VBE expands them automatically on import). Byte count is for final .BAS file, with CHR(10) as newline.

The subroutine, that can be invoked from VBE immediate window, opens a Sinclair BASIC program (first parameter is the path to an ASCII file - with CHR(10) as newline - containing the program), renumber lines and write results to a Variant variable (second parameter).

The idea is to iterate on all possible source line numbers, ascending order, and for each one, replace at once all matching line numbers as well as GO TO, GO SUB and RUN references with the next available target line number. Using this approach we do not need any kind of translation table. The target line number is incremented each time a match in source line number is found, so "wrong" line references are adjusted automatically to the next valid number. Newline characters are used as markers of start- and end-of-line, and a CHR(0) - never used in the program as it is not printable - is used as a temporary marker, to avoid renumbering the same line multiple times.

Some remarks:

  • For conciseness, we use the smaller possible string for a match with the jumping statements. Using the end-of-line on our search strings we do not run into the risk of including quoted occcurrences or user functions (that always use parenthesis in Sinclair). GO TO requires a larger string because of the FOR ... TO construct (e.g. compare 50 FOR X=AGO TO 100 and 50 GO TO 100)

  • The code does not support statements in the form GO TO200 (without whitespace), although the ZX manual implies that it is valid code on several examples (It would cost a dozen more bytes to deal with it).

  • The code adds a newline at the beginning and another at the end of the program. I could clean this up in the end (a dozen more bytes) but figure that the ZX would probably ignore blank lines.

Below, a more readable version:

Sub Renumber(ByVal ProgramPath As String, ByRef Program As Variant)
    
    Open ProgramPath For Input As #1
    Program = Chr(10) & Input(LOF(1), 1) & Chr(10)
    Close
    
    NewNumber = 10
    For OldNumber = 1 To 9999
        Program = Replace(Program, " GO TO" & OldNumber & Chr(10), " GO TO" & NewNumber & Chr(10)) 'self-explaining
        Program = Replace(Program, Chr(10) & OldNumber & " ", Chr(10) & NewNumber & Chr(0)) 'matches line number (and replaces whistespace with Chr(0) to avoid re-replacing
        Program = Replace(Program, "B " & OldNumber & Chr(10), "B " & NewNumber & Chr(10)) 'matches GO SUB
        Program = Replace(Program, "UN " & OldNumber & Chr(10), "UN " & NewNumber & Chr(10)) 'matches RUN
        If InStr(1, Program, Chr(10) & NewNumber & Chr(0)) Then NewNumber = NewNumber + 10 'if there is such a line, increment NewNumber
Next
Program = Replace(Program, Chr(0), " ") 'replace back Chr(0) with whitespace
End Sub
\$\endgroup\$
2
  • \$\begingroup\$ BTW, a QBasic solution would be much longer, as QBasic doesn't have a string-replacement function built in as far as I recall. \$\endgroup\$
    – DLosc
    Apr 23 '16 at 3:28
  • \$\begingroup\$ I think you're right... forgot about that \$\endgroup\$
    – dnep
    Apr 23 '16 at 3:44
1
\$\begingroup\$

Pip -rn, 63 bytes

Ygn:#{_<aFIy}*t+tgR`(RUN|GO (SUB|TO)) (\d+)$`{b.s.(nd)}R`^\d+`n

Try it online!

Explanation

Setup

The -r flag reads all of stdin and stores it as a list of lines in the local variable g. The global variable t is preinitialized to 10, and the global variable s is preinitialized to " ".

Yg

Yanks the list of lines g into the global variable y, so that it's available inside the function we're about to define.

Line number translation function

We construct a function that maps from any line number in the original numbering scheme (including a non-existent one) to the corresponding line number in the new numbering scheme.

Suppose we have these lines:

1 INPUT A
4 PRINT A
9 IF A=1 THEN GO TO 3

We want to map 1 to 10, 2-4 to 20, and 5-9 to 30. If we have a list of the original line numbers ([1; 4; 9]), we can use a filter operation to find out how many of these numbers are less than the line number we're trying to convert. Multiply that result by 10 and add 10, and we have the desired answer.

For example, when converting 9, there are two line numbers (1 and 4) less than 9. 2*10+10 gives 30. When converting 3, there is one line number (1) less than 3. 1*10+10 gives 20.

Here's the code (slightly modified to be easier to read):

n:{#(_<aFIy)*t+t}
  {             }  Lambda function with parameter a:
        FIy         Filter y (the list of program lines) for
     _<a             lines that are numerically less than a
                    (In a numeric context, only the first run of digits on the line is considered)
   #(      )        Number of items in the filtered list
            *t+t    Times 10, plus 10
n:                 Assign that function to global variable n

First replacement: GO TO, GO SUB, and RUN

The rest of the program is a single expression that takes g and does a couple of regex replacements (which vectorize, applying to each line in g).

Here's the first replacement:

g R `(RUN|GO (SUB|TO)) (\d+)$` {b.s.(nd)}

The regex matches any of RUN, GO SUB and GO TO, followed by a number, followed by end of line. This ensures it doesn't match inside strings, nor does it match RUN without a line number.

The order of the capturing groups is important. The first group captures the command (one of RUN, GO SUB, or GO TO). The second group, if used, captures either SUB or TO. We don't need to capture this part, but a non-capturing group would require extra bytes. Then the third group captures the line number.

We use a callback function for the replacement. With callback functions in Pip, the whole match is the first argument a, and the capture groups in order are the subsequent arguments b, c, d, and e. So we have the command in the first group, which goes in b, and the line number in the third group, which goes in d. The only change we need to make is to pass the line number through our conversion function, which is called Lisp-style: (nd). Then we concatenate that together with b and a space and return it.

Second replacement: line numbers

All that's left to convert is the line numbers at the beginnings of lines.

(...) R `^\d+` n

The regex matches a run of digits at the beginning of a line. Again we use a callback function; this time, the conversion function n itself is sufficient, since the whole match (first argument, a) is the number we want converted.

Since this is the last expression in the program, Pip autoprints the result. The -n flag separates the result list with newlines.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.