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