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