From 3f99c4800789a0527ab58a626b23c6a23a1f4130 Mon Sep 17 00:00:00 2001 From: Alice Vinogradova Date: Mon, 14 Sep 2026 12:24:18 +0100 Subject: [PATCH] GENERAL_GET_RANDOM_INT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A uniformly distributed integer between zero and RANGE, both included. The bounds are measured rather than inferred. The parameter documentation says "The random int will be <= range" and says nothing about the lower bound, and the obvious reading — one to range — is wrong. Three hundred calls per range on an AS ABAP 1909 sandbox: range 1 values 0..1 zero in 163 (a half) range 2 values 0..2 zero in 105 (a third) range 6 values 0..6 zero in 53 (a seventh) range 100 values 0..100 zero in 3 (a hundred-and-first) So the span is RANGE + 1 values. A range of zero answers zero, and a negative range is not an error there either: -5 answered -2, so the span runs towards RANGE on whichever side of zero it lies. The module declares no exceptions and the real one raises none. Written against Math.random directly rather than through cl_abap_random_int, which was the first attempt and does not fit: cl_abap_random=>intinrange opens with ASSERT high > low and ASSERT low >= 0, so a range of zero dumps and every negative range dumps. Reaching the contract through abs( ) and a negation would work and would read as cleverness hiding the intent. It is the same Math.random either way, since that is what cl_abap_random uses, and generate_sec_random in this group is the precedent for the plain form. Four tests in kernel_fugr_test: the zero range, the bounds at 6 and at -5, and that both 0 and 1 are reachable at range 1 — the last one being what tells this apart from a generator over 1..RANGE. --- .../kernel_fugr_test.clas.testclasses.abap | 84 +++++++++++++++++++ .../openabap.fugr.general_get_random_int.abap | 31 +++++++ src/fugr/openabap.fugr.xml | 17 ++++ 3 files changed, 132 insertions(+) create mode 100644 src/fugr/openabap.fugr.general_get_random_int.abap diff --git a/src/fugr/kernel_fugr_test.clas.testclasses.abap b/src/fugr/kernel_fugr_test.clas.testclasses.abap index b584626d..e092c24c 100644 --- a/src/fugr/kernel_fugr_test.clas.testclasses.abap +++ b/src/fugr/kernel_fugr_test.clas.testclasses.abap @@ -15,6 +15,10 @@ CLASS ltcl_fugr DEFINITION FOR TESTING RISK LEVEL HARMLESS DURATION SHORT FINAL. METHODS cexit_alpha_input_str FOR TESTING RAISING cx_static_check. METHODS cexit_alpha_input_str_in FOR TESTING RAISING cx_static_check. METHODS generate_sec_random FOR TESTING RAISING cx_static_check. + METHODS random_int_zero FOR TESTING RAISING cx_static_check. + METHODS random_int_one FOR TESTING RAISING cx_static_check. + METHODS random_int_six FOR TESTING RAISING cx_static_check. + METHODS random_int_negative FOR TESTING RAISING cx_static_check. METHODS text_split1 FOR TESTING RAISING cx_static_check. METHODS text_split2 FOR TESTING RAISING cx_static_check. METHODS function_exists_yes FOR TESTING RAISING cx_static_check. @@ -30,6 +34,86 @@ ENDCLASS. CLASS ltcl_fugr IMPLEMENTATION. + METHOD random_int_zero. +* the span is RANGE + 1 values, so a range of zero has exactly one + DATA lv_random TYPE i. + + CALL FUNCTION 'GENERAL_GET_RANDOM_INT' + EXPORTING + range = 0 + IMPORTING + random = lv_random. + + cl_abap_unit_assert=>assert_equals( + act = lv_random + exp = 0 ). + ENDMETHOD. + + METHOD random_int_one. +* zero is a legal answer and the distinguishing one: a generator over +* 1..RANGE would never produce it. Two hundred draws of a fair coin land on +* each side at least once with a certainty no test needs to worry about. + DATA lv_random TYPE i. + DATA lv_zeroes TYPE i. + DATA lv_ones TYPE i. + + DO 200 TIMES. + CALL FUNCTION 'GENERAL_GET_RANDOM_INT' + EXPORTING + range = 1 + IMPORTING + random = lv_random. + CASE lv_random. + WHEN 0. + lv_zeroes = lv_zeroes + 1. + WHEN 1. + lv_ones = lv_ones + 1. + WHEN OTHERS. + cl_abap_unit_assert=>fail( msg = |out of range: { lv_random }| ). + ENDCASE. + ENDDO. + + cl_abap_unit_assert=>assert_differs( + act = lv_zeroes + exp = 0 + msg = 'zero must be reachable' ). + cl_abap_unit_assert=>assert_differs( + act = lv_ones + exp = 0 + msg = 'the range itself must be reachable' ). + ENDMETHOD. + + METHOD random_int_six. + DATA lv_random TYPE i. + + DO 200 TIMES. + CALL FUNCTION 'GENERAL_GET_RANDOM_INT' + EXPORTING + range = 6 + IMPORTING + random = lv_random. + IF lv_random < 0 OR lv_random > 6. + cl_abap_unit_assert=>fail( msg = |out of range: { lv_random }| ). + ENDIF. + ENDDO. + ENDMETHOD. + + METHOD random_int_negative. +* a negative range is not an error: the span runs towards RANGE + DATA lv_random TYPE i. + + DO 200 TIMES. + CALL FUNCTION 'GENERAL_GET_RANDOM_INT' + EXPORTING + range = -5 + IMPORTING + random = lv_random. + IF lv_random < -5 OR lv_random > 0. + cl_abap_unit_assert=>fail( msg = |out of range: { lv_random }| ). + ENDIF. + ENDDO. + ENDMETHOD. + METHOD text_split1. CONSTANTS lc_text TYPE c LENGTH 200 VALUE '01234567890123456789012345678901234567890123456789 123456789'. DATA lv_line TYPE string. diff --git a/src/fugr/openabap.fugr.general_get_random_int.abap b/src/fugr/openabap.fugr.general_get_random_int.abap new file mode 100644 index 00000000..177d2021 --- /dev/null +++ b/src/fugr/openabap.fugr.general_get_random_int.abap @@ -0,0 +1,31 @@ +FUNCTION general_get_random_int. + +* A uniformly distributed integer between zero and RANGE, both included. +* +* Measured against an AS ABAP 1909 rather than inferred, because the obvious +* reading of the parameter text ("The random int will be <= range") is one to +* range, and that is wrong. Three hundred calls per range: +* +* range 1 values 0..1 zero in 163 (a half) +* range 2 values 0..2 zero in 105 (a third) +* range 6 values 0..6 zero in 53 (a seventh) +* range 100 values 0..100 zero in 3 (a hundred-and-first) +* +* The span is RANGE + 1 values. A range of zero answers zero, and a negative +* range is not an error: -5 answered -2, so the span runs towards RANGE on +* whichever side of zero it lies. The module declares no exceptions and the +* real one raises none. +* +* Written against Math.random directly rather than through +* cl_abap_random_int, which was the first attempt. That class cannot express +* this: cl_abap_random=>intinrange opens with ASSERT high > low and +* ASSERT low >= 0, so a range of zero dumps and every negative range dumps. +* Reaching the contract through abs( ) and a negation would work and would +* read as cleverness hiding the intent. The randomness is the same +* Math.random either way, since that is what cl_abap_random uses too, and +* generate_sec_random in this group is the precedent for the plain form. + WRITE '@KERNEL const r = range.get();'. + WRITE '@KERNEL const span = Math.abs(r) + 1;'. + WRITE '@KERNEL random.set(Math.sign(r) * Math.floor(Math.random() * span));'. + +ENDFUNCTION. diff --git a/src/fugr/openabap.fugr.xml b/src/fugr/openabap.fugr.xml index c7fc23dd..c8f8c643 100644 --- a/src/fugr/openabap.fugr.xml +++ b/src/fugr/openabap.fugr.xml @@ -103,6 +103,23 @@ + + GENERAL_GET_RANDOM_INT + + + RANGE + X + I + + + + + RANDOM + X + I + + + GENERATE_SEC_RANDOM