⚠️ Warning: This is a draft ⚠️

This means it might contain formatting issues, incorrect code, conceptual problems, or other severe issues.

If you want to help to improve and eventually enable this page, please fork RosettaGit's repository and open a merge request on GitHub.

This is essentially a modified port of the C version.

Firstly we need to make a Reverse Polish Notation parser. To make it easier, I simply put this in the report used in the game itself. The following data declarations should be common to both for ease of use.

Global Data

report z24_with_rpn
constants: c_eval_to   type i value 24,
           c_tolerance type f value '0.0001'.
data: gt_val type table of f,
      gv_val type f,
      gv_pac type p,
      gv_chk type c.

RPN Code

" Log a message from the RPN Calculator.
form rpn_log using lv_msg type string.
  write : / 'RPN Message: ', lv_msg.
endform.

" Performs add in Reverse Polish Notation.
form rpn_add.
  data: lv_val1 type f,
        lv_val2 type f.
  " Get the last two values from the stack to add together.
  perform rpn_pop changing: lv_val1, lv_val2.
  add lv_val2 to lv_val1.
  " Add them and then add them back to the "top".
  perform rpn_push using lv_val1.
endform.

" Perform subtraction in RPN.
form rpn_sub.
  data: lv_val1 type f,
        lv_val2 type f.
  " Get the last two values, subtract them, and push them back on.
  perform rpn_pop changing: lv_val1, lv_val2.
  subtract lv_val1 from lv_val2.
  perform rpn_push using lv_val2.
endform.

" Perform multiplication in RPN.
form rpn_mul.
  data: lv_val1 type f,
        lv_val2 type f.
  " Get the last two values, multiply, and push them back.
  perform rpn_pop changing: lv_val1, lv_val2.
  multiply lv_val1 by lv_val2.
  perform rpn_push using lv_val1.
endform.

" Perform division in RPN.
form rpn_div.
  data: lv_val1 type f,
        lv_val2 type f.
  " Get the last two values, divide the first by the second
  " and then add it back to the stack.
  perform rpn_pop changing: lv_val1, lv_val2.
  divide lv_val1 by lv_val2.
  perform rpn_push using lv_val1.
endform.

" Negate a number in RPN.
form rpn_neg.
  data: lv_val type f.
  " Simply get the last number and negate it before pushing it back.
  perform rpn_pop changing lv_val.
  multiply lv_val by -1.
  perform rpn_push using lv_val.
endform.

" Swap the top two values on the RPN Stack.
form rpn_swap.
  data: lv_val1 type f,
        lv_val2 type f.
  " Get the top two values and then add them back in reverse order.
  perform rpn_pop changing: lv_val1, lv_val2.
  perform rpn_push using: lv_val2, lv_val1.
endform.

" Call the relevant RPN operation.
form rpn_call_op using iv_op type string.
  case iv_op.
    when '+'.
      perform rpn_add.
    when '-'.
      perform rpn_sub.
    when '*'.
      perform rpn_mul.
    when '/'.
      perform rpn_div.
    when 'n'.
      perform rpn_neg.
    when 's'.
      perform rpn_swap.
    when others. " Bad op-code found!
      perform rpn_log using 'Operation not found!'.
    endcase.
endform.

" Reverse_Polish_Notation Parser.
form rpn_pop changing ev_out type f.
  " Attempt to get the entry from the 'top' of the table.
  " If it's empty --> log an error and bail.
  data: lv_lines type i.

  describe table gt_val lines lv_lines.
  if lv_lines > 0.
    " After we have retrieved the value, we must remove it from the table.
    read table gt_val index lv_lines into ev_out.
    delete gt_val index lv_lines.
  else.
    perform rpn_log using 'RPN Stack is empty! Underflow!'.
    ev_out = 0.
  endif.
endform.

" Pushes the supplied value onto the RPN table / stack.
form rpn_push using iv_val type f.
  " Simple append - other languages this involves a stack of a certain size.
  append iv_val to gt_val.
endform.

" Refreshes the RPN stack / table.
form rpn_reset.
  " Clear the stack to start anew.
  refresh gt_val.
endform.

" Checks if the supplied string is numeric.
" Lazy evaluation - only checkcs for numbers without formatting.
form rpn_numeric using    iv_in  type string
                 changing ev_out type c.
  data: lv_moff type i,
        lv_len  type i.
  " Match digits with optional decimal places.
  find regex '\d+(\.\d+)*' in iv_in
    match offset lv_moff
    match length lv_len.
  " Get the offset and length of the first occurence, and work
  " out the length of the match.
  subtract lv_moff from lv_len.
  " If the length is different to the length of the whole string,
  " then it's NOT a match, else it is.
  if lv_len ne strlen( iv_in ).
    ev_out = ' '.
  else.
    ev_out = 'X'.
  endif.
endform.

" Convert input to a number. Added safety net of is_numeric.
form rpn_get_num using iv_in type string changing ev_num type f.
  data: lv_check type c.
  " Check if it's numeric - built in redundancy.
  perform rpn_numeric using iv_in changing lv_check.
  if lv_check = 'X'.
    ev_num = iv_in.
  else.
    perform rpn_log using 'Wrong call!'.
  endif.
endform.

" Evaluate the RPN expression and return true if success in eval.
form rpn_eval using in_expr type string changing ev_out type c.
  data: lv_len  type i,
        lv_off  type i value 0,
        lv_num  type c,
        lv_val  type f,
        lv_tok  type string.

  lv_len = strlen( in_expr ).
  do lv_len times.
    lv_tok = in_expr+lv_off(1).
    perform rpn_numeric using lv_tok changing lv_num.
    if lv_num = 'X'.
      perform: rpn_get_num using lv_tok changing lv_val,
               rpn_push    using lv_val.
    else.

      perform rpn_call_op using lv_tok.
    endif.
    add 1 to lv_off.
  enddo.

  ev_out = 'X'.
endform.

24 Game

We can now play the game since we have a parser. The interface is a hacked up screen, and is a bit more clunky than even a CLI (No such option for ABAP, at least not which I'm aware of).

The supplied Random Number Generator seems to highly favour a five as the first digit as well (It does occasionally take on other values). It doesn't appear to be a seeding issue, as the other numbers appear sufficiently random.


selection-screen begin of block main with frame title lv_title.
  parameters:
    p_first  type i,
    p_second type i,
    p_third  type i,
    p_fourth type i,
    p_expr   type string.
selection-screen end of block main.

initialization.
  perform ranged_rand using 1 9 changing p_first.
  perform ranged_rand using 1 9 changing p_second.
  perform ranged_rand using 1 9 changing p_third.
  perform ranged_rand using 1 9 changing p_fourth.

at selection-screen output.
  " Set-up paramter texts.
  lv_title = 'Reverse Polish Notation Tester - Enter expression that evaluates to 24.'.
  %_p_first_%_app_%-text  = 'First Number: '.
  %_p_second_%_app_%-text = 'Second Number: '.
  %_p_third_%_app_%-text  = 'Third Number: '.
  %_p_fourth_%_app_%-text = 'Fourth Number: '.
  %_p_expr_%_app_%-text   = 'Expression: '.
  " Disallow modification of supplied numbers.
  loop at screen.
    if screen-name = 'P_FIRST' or  screen-name = 'P_SECOND' or
       screen-name = 'P_THIRD' or  screen-name = 'P_FOURTH'.
      screen-input = '0'.
      modify screen.
    endif.
  endloop.

start-of-selection.
  " Check the expression is valid.
  perform check_expr using p_expr changing gv_chk.
  if gv_chk <> 'X'.
    write : / 'Invalid input!'.
    stop.
  endif.
  " Check if the expression actually evalutes.
  perform rpn_eval using p_expr changing gv_chk.
  " If it doesn't, warning!.
  if gv_chk <> 'X'.
    write : / 'Invalid expression!'.
    stop.
  endif.
  " Get the evaluated value. Transform it to something that displays a bit better.
  " Then check if it's a valid answer, with a certain tolerance.
  " If they're wrong, give them instructions to on how to go back.
  perform rpn_pop changing gv_val.
  gv_pac = gv_val.
  gv_val = abs( gv_val - c_eval_to ).
  if gv_val < c_tolerance.
    write : / 'Answer correct'.
  else.
    write : / 'Your expression evalutes to ', gv_pac.
    write : / 'Press "F3" to go back and try again!'.
  endif.
  write : / 'Re-run the program to generate a new set.'.

 " Check that the input expression is valid - i.e. all supplied numbers
 " appears exactly once. This does not validate the expression itself.
form check_expr using iv_exp type string changing ev_ok type c.
  data: lv_chk  type c,
        lv_tok  type string,
        lv_val  type i value 0,
        lv_len  type i,
        lv_off  type i,
        lv_num  type i,
        lt_nums type standard table of i.
  ev_ok = 'X'.
  " Update the number count table - indexes 1-9 correspond to numbers.
  " The value stored corresponds to the number of occurences.
  do 9 times.
    if p_first = sy-index.
      add 1 to lv_val.
    endif.
    if p_second = sy-index.
      add 1 to lv_val.
    endif.
    if p_third = sy-index.
      add 1 to lv_val.
    endif.
    if p_fourth = sy-index.
      add 1 to lv_val.
    endif.

    append lv_val to lt_nums.
    lv_val = 0.
  enddo.
  " Loop through the expression parsing the numbers.
  lv_len = strlen( p_expr ).
  do lv_len times.
    lv_tok = p_expr+lv_off(1). " Check if the current token is a number.
    perform rpn_numeric using lv_tok changing lv_chk.
    if lv_chk = 'X'.
      lv_num = lv_tok. " If it's a number, it must be from 1 - 9.
      if lv_num < 1 or lv_num > 9.
        ev_ok = ' '.
        write : / 'Numbers must be between 1 and 9!'.
        return.
      else.
        " Check how many times the number was supplied. If it wasn't supplied
        " or if we have used it up, we should give an error.
        read table lt_nums index lv_num into lv_val.
        if lv_val <= 0.
          ev_ok = ' '.
          write : / 'You can not use numbers more than once'.
          return.
        endif.
        " If we have values left for this number, we decrement the remaining amount.
        subtract 1 from lv_val.
        modify lt_nums index lv_num from lv_val.
      endif.
    endif.
    add 1 to lv_off.
  enddo.
  " Loop through the table and check we have no numbers left for use.
  do 9 times.
    read table lt_nums index  sy-index into lv_val.
    if lv_val > 0.
      write : / 'You must use all numbers'.
      ev_ok = ' '.
      return.
     endif.
  enddo.
endform.

" Generate a random number within the given range.
form ranged_rand using iv_min type i iv_max type i
                 changing ev_val type i.
  call function 'QF05_RANDOM_INTEGER'
    exporting
      ran_int_max = iv_max
      ran_int_min = iv_min
    importing
      ran_int     = ev_val.
endform.