Here my solution with 3 regular expression:
str.replaceAll("([^A-Z])([A-Z0-9])", "$1_$2") // standard replace
.replaceAll("([A-Z]+)([A-Z0-9][^A-Z]+)", "$1_$2") // last letter after full uppercase.
.replaceAll("([0-9]+)([a-zA-Z]+)", "$1_$2").toLowerCase(); // letters after numbers
The result:
thisIsATest: this_is_a_test
EndWithNumber3: end_with_number_3
3ThisStartWithNumber: 3_this_start_with_number
Number3InMiddle: number_3_in_middle
Number3inMiddleAgain: number_3_in_middle_again
MyUUIDNot: my_uuid_not
HOLAMundo: hola_mundo
holaMUNDO: hola_mundo
with_underscore: with_underscore
withAUniqueLetter: with_a_unique_letter
Edited:
To support numbers and another symbols, you can use this:
str.replaceAll("([^A-Z])([A-Z])", "$1_$2") // standard replace
.replaceAll("([A-Z]+)([^a-z][^A-Z]+)", "$1_$2") // last letter after full uppercase.
.toLowerCase()
.replaceAll("([^a-z]+)([a-z]+)", "$1_$2") // letters after non-letters.
.replaceAll("([a-z]+)([^a-z]+)", "$1_$2"); // letters before non-letters.
The result:
thisIsATest: "this_is_a_test"
EndWithNumber3: "end_with_number_3"
3ThisStartWithNumber: "3_this_start_with_number"
Number3InMiddle: "number_3_in_middle"
Number3inMiddleAgain: "number_3_in_middle_again"
MyUUIDNot: "my_uuid_not"
HOLAMundo: "hola_mundo"
holaMUNDO: "hola_mundo"
with_underscore: "with_underscore"
withAUniqueLetter: "with_a_unique_letter"
with%SYMBOLAndNumber90: "with_%_symbol_and_number_90"
http%: "http_%"
123456789: "123456789"
: " "
_: "_"
__abc__: "__abc__"