diff options
Diffstat (limited to 'base')
187 files changed, 23579 insertions, 0 deletions
diff --git a/base/.amateras b/base/.amateras new file mode 100644 index 0000000..b00142f --- /dev/null +++ b/base/.amateras @@ -0,0 +1,11 @@ +#EclipseHTMLEditor configuration file +#Sat Sep 24 10:15:28 EDT 2016 +validateDTD=true +useDTD=true +validateJSP=true +validateXML=true +validateJS=true +removeMarkers=false +root=/ +validateHTML=true +javaScripts= diff --git a/base/.gitignore b/base/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/base/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/base/data/BidiMirrorDB.txt b/base/data/BidiMirrorDB.txt new file mode 100644 index 0000000..68142c5 --- /dev/null +++ b/base/data/BidiMirrorDB.txt @@ -0,0 +1,606 @@ +# BidiMirroring-9.0.0.txt +# Date: 2016-01-21, 22:00:00 GMT [KW, LI] +# © 2016 Unicode®, Inc. +# For terms of use, see http://www.unicode.org/terms_of_use.html +# +# Unicode Character Database +# For documentation, see http://www.unicode.org/reports/tr44/ +# +# Bidi_Mirroring_Glyph Property +# +# This file is an informative contributory data file in the +# Unicode Character Database. +# +# This data file lists characters that have the Bidi_Mirrored=Yes property +# value, for which there is another Unicode character that typically has a glyph +# that is the mirror image of the original character's glyph. +# +# The repertoire covered by the file is Unicode 9.0.0. +# +# The file contains a list of lines with mappings from one code point +# to another one for character-based mirroring. +# Note that for "real" mirroring, a rendering engine needs to select +# appropriate alternative glyphs, and that many Unicode characters do not +# have a mirror-image Unicode character. +# +# Each mapping line contains two fields, separated by a semicolon (';'). +# Each of the two fields contains a code point represented as a +# variable-length hexadecimal value with 4 to 6 digits. +# A comment indicates where the characters are "BEST FIT" mirroring. +# +# Code points for which Bidi_Mirrored=Yes, but for which no appropriate +# characters exist with mirrored glyphs, are +# listed as comments at the end of the file. +# +# Formally, the default value of the Bidi_Mirroring_Glyph property +# for each code point is <none>, unless a mapping to +# some other character is specified in this data file. When a code +# point has the default value for the Bidi_Mirroring_Glyph property, +# that means that no other character exists whose glyph is suitable +# for character-based mirroring. +# +# For information on bidi mirroring, see UAX #9: Unicode Bidirectional Algorithm, +# at http://www.unicode.org/unicode/reports/tr9/ +# +# This file was originally created by Markus Scherer. +# Extended for Unicode 3.2, 4.0, 4.1, 5.0, 5.1, 5.2, and 6.0 by Ken Whistler, +# and for subsequent versions by Ken Whistler and Laurentiu Iancu. +# +# ############################################################ +# +# Property: Bidi_Mirroring_Glyph +# +# @missing: 0000..10FFFF; <none> + +0028; 0029 # LEFT PARENTHESIS +0029; 0028 # RIGHT PARENTHESIS +003C; 003E # LESS-THAN SIGN +003E; 003C # GREATER-THAN SIGN +005B; 005D # LEFT SQUARE BRACKET +005D; 005B # RIGHT SQUARE BRACKET +007B; 007D # LEFT CURLY BRACKET +007D; 007B # RIGHT CURLY BRACKET +00AB; 00BB # LEFT-POINTING DOUBLE ANGLE QUOTATION MARK +00BB; 00AB # RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK +0F3A; 0F3B # TIBETAN MARK GUG RTAGS GYON +0F3B; 0F3A # TIBETAN MARK GUG RTAGS GYAS +0F3C; 0F3D # TIBETAN MARK ANG KHANG GYON +0F3D; 0F3C # TIBETAN MARK ANG KHANG GYAS +169B; 169C # OGHAM FEATHER MARK +169C; 169B # OGHAM REVERSED FEATHER MARK +2039; 203A # SINGLE LEFT-POINTING ANGLE QUOTATION MARK +203A; 2039 # SINGLE RIGHT-POINTING ANGLE QUOTATION MARK +2045; 2046 # LEFT SQUARE BRACKET WITH QUILL +2046; 2045 # RIGHT SQUARE BRACKET WITH QUILL +207D; 207E # SUPERSCRIPT LEFT PARENTHESIS +207E; 207D # SUPERSCRIPT RIGHT PARENTHESIS +208D; 208E # SUBSCRIPT LEFT PARENTHESIS +208E; 208D # SUBSCRIPT RIGHT PARENTHESIS +2208; 220B # ELEMENT OF +2209; 220C # NOT AN ELEMENT OF +220A; 220D # SMALL ELEMENT OF +220B; 2208 # CONTAINS AS MEMBER +220C; 2209 # DOES NOT CONTAIN AS MEMBER +220D; 220A # SMALL CONTAINS AS MEMBER +2215; 29F5 # DIVISION SLASH +223C; 223D # TILDE OPERATOR +223D; 223C # REVERSED TILDE +2243; 22CD # ASYMPTOTICALLY EQUAL TO +2252; 2253 # APPROXIMATELY EQUAL TO OR THE IMAGE OF +2253; 2252 # IMAGE OF OR APPROXIMATELY EQUAL TO +2254; 2255 # COLON EQUALS +2255; 2254 # EQUALS COLON +2264; 2265 # LESS-THAN OR EQUAL TO +2265; 2264 # GREATER-THAN OR EQUAL TO +2266; 2267 # LESS-THAN OVER EQUAL TO +2267; 2266 # GREATER-THAN OVER EQUAL TO +2268; 2269 # [BEST FIT] LESS-THAN BUT NOT EQUAL TO +2269; 2268 # [BEST FIT] GREATER-THAN BUT NOT EQUAL TO +226A; 226B # MUCH LESS-THAN +226B; 226A # MUCH GREATER-THAN +226E; 226F # [BEST FIT] NOT LESS-THAN +226F; 226E # [BEST FIT] NOT GREATER-THAN +2270; 2271 # [BEST FIT] NEITHER LESS-THAN NOR EQUAL TO +2271; 2270 # [BEST FIT] NEITHER GREATER-THAN NOR EQUAL TO +2272; 2273 # [BEST FIT] LESS-THAN OR EQUIVALENT TO +2273; 2272 # [BEST FIT] GREATER-THAN OR EQUIVALENT TO +2274; 2275 # [BEST FIT] NEITHER LESS-THAN NOR EQUIVALENT TO +2275; 2274 # [BEST FIT] NEITHER GREATER-THAN NOR EQUIVALENT TO +2276; 2277 # LESS-THAN OR GREATER-THAN +2277; 2276 # GREATER-THAN OR LESS-THAN +2278; 2279 # [BEST FIT] NEITHER LESS-THAN NOR GREATER-THAN +2279; 2278 # [BEST FIT] NEITHER GREATER-THAN NOR LESS-THAN +227A; 227B # PRECEDES +227B; 227A # SUCCEEDS +227C; 227D # PRECEDES OR EQUAL TO +227D; 227C # SUCCEEDS OR EQUAL TO +227E; 227F # [BEST FIT] PRECEDES OR EQUIVALENT TO +227F; 227E # [BEST FIT] SUCCEEDS OR EQUIVALENT TO +2280; 2281 # [BEST FIT] DOES NOT PRECEDE +2281; 2280 # [BEST FIT] DOES NOT SUCCEED +2282; 2283 # SUBSET OF +2283; 2282 # SUPERSET OF +2284; 2285 # [BEST FIT] NOT A SUBSET OF +2285; 2284 # [BEST FIT] NOT A SUPERSET OF +2286; 2287 # SUBSET OF OR EQUAL TO +2287; 2286 # SUPERSET OF OR EQUAL TO +2288; 2289 # [BEST FIT] NEITHER A SUBSET OF NOR EQUAL TO +2289; 2288 # [BEST FIT] NEITHER A SUPERSET OF NOR EQUAL TO +228A; 228B # [BEST FIT] SUBSET OF WITH NOT EQUAL TO +228B; 228A # [BEST FIT] SUPERSET OF WITH NOT EQUAL TO +228F; 2290 # SQUARE IMAGE OF +2290; 228F # SQUARE ORIGINAL OF +2291; 2292 # SQUARE IMAGE OF OR EQUAL TO +2292; 2291 # SQUARE ORIGINAL OF OR EQUAL TO +2298; 29B8 # CIRCLED DIVISION SLASH +22A2; 22A3 # RIGHT TACK +22A3; 22A2 # LEFT TACK +22A6; 2ADE # ASSERTION +22A8; 2AE4 # TRUE +22A9; 2AE3 # FORCES +22AB; 2AE5 # DOUBLE VERTICAL BAR DOUBLE RIGHT TURNSTILE +22B0; 22B1 # PRECEDES UNDER RELATION +22B1; 22B0 # SUCCEEDS UNDER RELATION +22B2; 22B3 # NORMAL SUBGROUP OF +22B3; 22B2 # CONTAINS AS NORMAL SUBGROUP +22B4; 22B5 # NORMAL SUBGROUP OF OR EQUAL TO +22B5; 22B4 # CONTAINS AS NORMAL SUBGROUP OR EQUAL TO +22B6; 22B7 # ORIGINAL OF +22B7; 22B6 # IMAGE OF +22C9; 22CA # LEFT NORMAL FACTOR SEMIDIRECT PRODUCT +22CA; 22C9 # RIGHT NORMAL FACTOR SEMIDIRECT PRODUCT +22CB; 22CC # LEFT SEMIDIRECT PRODUCT +22CC; 22CB # RIGHT SEMIDIRECT PRODUCT +22CD; 2243 # REVERSED TILDE EQUALS +22D0; 22D1 # DOUBLE SUBSET +22D1; 22D0 # DOUBLE SUPERSET +22D6; 22D7 # LESS-THAN WITH DOT +22D7; 22D6 # GREATER-THAN WITH DOT +22D8; 22D9 # VERY MUCH LESS-THAN +22D9; 22D8 # VERY MUCH GREATER-THAN +22DA; 22DB # LESS-THAN EQUAL TO OR GREATER-THAN +22DB; 22DA # GREATER-THAN EQUAL TO OR LESS-THAN +22DC; 22DD # EQUAL TO OR LESS-THAN +22DD; 22DC # EQUAL TO OR GREATER-THAN +22DE; 22DF # EQUAL TO OR PRECEDES +22DF; 22DE # EQUAL TO OR SUCCEEDS +22E0; 22E1 # [BEST FIT] DOES NOT PRECEDE OR EQUAL +22E1; 22E0 # [BEST FIT] DOES NOT SUCCEED OR EQUAL +22E2; 22E3 # [BEST FIT] NOT SQUARE IMAGE OF OR EQUAL TO +22E3; 22E2 # [BEST FIT] NOT SQUARE ORIGINAL OF OR EQUAL TO +22E4; 22E5 # [BEST FIT] SQUARE IMAGE OF OR NOT EQUAL TO +22E5; 22E4 # [BEST FIT] SQUARE ORIGINAL OF OR NOT EQUAL TO +22E6; 22E7 # [BEST FIT] LESS-THAN BUT NOT EQUIVALENT TO +22E7; 22E6 # [BEST FIT] GREATER-THAN BUT NOT EQUIVALENT TO +22E8; 22E9 # [BEST FIT] PRECEDES BUT NOT EQUIVALENT TO +22E9; 22E8 # [BEST FIT] SUCCEEDS BUT NOT EQUIVALENT TO +22EA; 22EB # [BEST FIT] NOT NORMAL SUBGROUP OF +22EB; 22EA # [BEST FIT] DOES NOT CONTAIN AS NORMAL SUBGROUP +22EC; 22ED # [BEST FIT] NOT NORMAL SUBGROUP OF OR EQUAL TO +22ED; 22EC # [BEST FIT] DOES NOT CONTAIN AS NORMAL SUBGROUP OR EQUAL +22F0; 22F1 # UP RIGHT DIAGONAL ELLIPSIS +22F1; 22F0 # DOWN RIGHT DIAGONAL ELLIPSIS +22F2; 22FA # ELEMENT OF WITH LONG HORIZONTAL STROKE +22F3; 22FB # ELEMENT OF WITH VERTICAL BAR AT END OF HORIZONTAL STROKE +22F4; 22FC # SMALL ELEMENT OF WITH VERTICAL BAR AT END OF HORIZONTAL STROKE +22F6; 22FD # ELEMENT OF WITH OVERBAR +22F7; 22FE # SMALL ELEMENT OF WITH OVERBAR +22FA; 22F2 # CONTAINS WITH LONG HORIZONTAL STROKE +22FB; 22F3 # CONTAINS WITH VERTICAL BAR AT END OF HORIZONTAL STROKE +22FC; 22F4 # SMALL CONTAINS WITH VERTICAL BAR AT END OF HORIZONTAL STROKE +22FD; 22F6 # CONTAINS WITH OVERBAR +22FE; 22F7 # SMALL CONTAINS WITH OVERBAR +2308; 2309 # LEFT CEILING +2309; 2308 # RIGHT CEILING +230A; 230B # LEFT FLOOR +230B; 230A # RIGHT FLOOR +2329; 232A # LEFT-POINTING ANGLE BRACKET +232A; 2329 # RIGHT-POINTING ANGLE BRACKET +2768; 2769 # MEDIUM LEFT PARENTHESIS ORNAMENT +2769; 2768 # MEDIUM RIGHT PARENTHESIS ORNAMENT +276A; 276B # MEDIUM FLATTENED LEFT PARENTHESIS ORNAMENT +276B; 276A # MEDIUM FLATTENED RIGHT PARENTHESIS ORNAMENT +276C; 276D # MEDIUM LEFT-POINTING ANGLE BRACKET ORNAMENT +276D; 276C # MEDIUM RIGHT-POINTING ANGLE BRACKET ORNAMENT +276E; 276F # HEAVY LEFT-POINTING ANGLE QUOTATION MARK ORNAMENT +276F; 276E # HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT +2770; 2771 # HEAVY LEFT-POINTING ANGLE BRACKET ORNAMENT +2771; 2770 # HEAVY RIGHT-POINTING ANGLE BRACKET ORNAMENT +2772; 2773 # LIGHT LEFT TORTOISE SHELL BRACKET ORNAMENT +2773; 2772 # LIGHT RIGHT TORTOISE SHELL BRACKET ORNAMENT +2774; 2775 # MEDIUM LEFT CURLY BRACKET ORNAMENT +2775; 2774 # MEDIUM RIGHT CURLY BRACKET ORNAMENT +27C3; 27C4 # OPEN SUBSET +27C4; 27C3 # OPEN SUPERSET +27C5; 27C6 # LEFT S-SHAPED BAG DELIMITER +27C6; 27C5 # RIGHT S-SHAPED BAG DELIMITER +27C8; 27C9 # REVERSE SOLIDUS PRECEDING SUBSET +27C9; 27C8 # SUPERSET PRECEDING SOLIDUS +27CB; 27CD # MATHEMATICAL RISING DIAGONAL +27CD; 27CB # MATHEMATICAL FALLING DIAGONAL +27D5; 27D6 # LEFT OUTER JOIN +27D6; 27D5 # RIGHT OUTER JOIN +27DD; 27DE # LONG RIGHT TACK +27DE; 27DD # LONG LEFT TACK +27E2; 27E3 # WHITE CONCAVE-SIDED DIAMOND WITH LEFTWARDS TICK +27E3; 27E2 # WHITE CONCAVE-SIDED DIAMOND WITH RIGHTWARDS TICK +27E4; 27E5 # WHITE SQUARE WITH LEFTWARDS TICK +27E5; 27E4 # WHITE SQUARE WITH RIGHTWARDS TICK +27E6; 27E7 # MATHEMATICAL LEFT WHITE SQUARE BRACKET +27E7; 27E6 # MATHEMATICAL RIGHT WHITE SQUARE BRACKET +27E8; 27E9 # MATHEMATICAL LEFT ANGLE BRACKET +27E9; 27E8 # MATHEMATICAL RIGHT ANGLE BRACKET +27EA; 27EB # MATHEMATICAL LEFT DOUBLE ANGLE BRACKET +27EB; 27EA # MATHEMATICAL RIGHT DOUBLE ANGLE BRACKET +27EC; 27ED # MATHEMATICAL LEFT WHITE TORTOISE SHELL BRACKET +27ED; 27EC # MATHEMATICAL RIGHT WHITE TORTOISE SHELL BRACKET +27EE; 27EF # MATHEMATICAL LEFT FLATTENED PARENTHESIS +27EF; 27EE # MATHEMATICAL RIGHT FLATTENED PARENTHESIS +2983; 2984 # LEFT WHITE CURLY BRACKET +2984; 2983 # RIGHT WHITE CURLY BRACKET +2985; 2986 # LEFT WHITE PARENTHESIS +2986; 2985 # RIGHT WHITE PARENTHESIS +2987; 2988 # Z NOTATION LEFT IMAGE BRACKET +2988; 2987 # Z NOTATION RIGHT IMAGE BRACKET +2989; 298A # Z NOTATION LEFT BINDING BRACKET +298A; 2989 # Z NOTATION RIGHT BINDING BRACKET +298B; 298C # LEFT SQUARE BRACKET WITH UNDERBAR +298C; 298B # RIGHT SQUARE BRACKET WITH UNDERBAR +298D; 2990 # LEFT SQUARE BRACKET WITH TICK IN TOP CORNER +298E; 298F # RIGHT SQUARE BRACKET WITH TICK IN BOTTOM CORNER +298F; 298E # LEFT SQUARE BRACKET WITH TICK IN BOTTOM CORNER +2990; 298D # RIGHT SQUARE BRACKET WITH TICK IN TOP CORNER +2991; 2992 # LEFT ANGLE BRACKET WITH DOT +2992; 2991 # RIGHT ANGLE BRACKET WITH DOT +2993; 2994 # LEFT ARC LESS-THAN BRACKET +2994; 2993 # RIGHT ARC GREATER-THAN BRACKET +2995; 2996 # DOUBLE LEFT ARC GREATER-THAN BRACKET +2996; 2995 # DOUBLE RIGHT ARC LESS-THAN BRACKET +2997; 2998 # LEFT BLACK TORTOISE SHELL BRACKET +2998; 2997 # RIGHT BLACK TORTOISE SHELL BRACKET +29B8; 2298 # CIRCLED REVERSE SOLIDUS +29C0; 29C1 # CIRCLED LESS-THAN +29C1; 29C0 # CIRCLED GREATER-THAN +29C4; 29C5 # SQUARED RISING DIAGONAL SLASH +29C5; 29C4 # SQUARED FALLING DIAGONAL SLASH +29CF; 29D0 # LEFT TRIANGLE BESIDE VERTICAL BAR +29D0; 29CF # VERTICAL BAR BESIDE RIGHT TRIANGLE +29D1; 29D2 # BOWTIE WITH LEFT HALF BLACK +29D2; 29D1 # BOWTIE WITH RIGHT HALF BLACK +29D4; 29D5 # TIMES WITH LEFT HALF BLACK +29D5; 29D4 # TIMES WITH RIGHT HALF BLACK +29D8; 29D9 # LEFT WIGGLY FENCE +29D9; 29D8 # RIGHT WIGGLY FENCE +29DA; 29DB # LEFT DOUBLE WIGGLY FENCE +29DB; 29DA # RIGHT DOUBLE WIGGLY FENCE +29F5; 2215 # REVERSE SOLIDUS OPERATOR +29F8; 29F9 # BIG SOLIDUS +29F9; 29F8 # BIG REVERSE SOLIDUS +29FC; 29FD # LEFT-POINTING CURVED ANGLE BRACKET +29FD; 29FC # RIGHT-POINTING CURVED ANGLE BRACKET +2A2B; 2A2C # MINUS SIGN WITH FALLING DOTS +2A2C; 2A2B # MINUS SIGN WITH RISING DOTS +2A2D; 2A2E # PLUS SIGN IN LEFT HALF CIRCLE +2A2E; 2A2D # PLUS SIGN IN RIGHT HALF CIRCLE +2A34; 2A35 # MULTIPLICATION SIGN IN LEFT HALF CIRCLE +2A35; 2A34 # MULTIPLICATION SIGN IN RIGHT HALF CIRCLE +2A3C; 2A3D # INTERIOR PRODUCT +2A3D; 2A3C # RIGHTHAND INTERIOR PRODUCT +2A64; 2A65 # Z NOTATION DOMAIN ANTIRESTRICTION +2A65; 2A64 # Z NOTATION RANGE ANTIRESTRICTION +2A79; 2A7A # LESS-THAN WITH CIRCLE INSIDE +2A7A; 2A79 # GREATER-THAN WITH CIRCLE INSIDE +2A7D; 2A7E # LESS-THAN OR SLANTED EQUAL TO +2A7E; 2A7D # GREATER-THAN OR SLANTED EQUAL TO +2A7F; 2A80 # LESS-THAN OR SLANTED EQUAL TO WITH DOT INSIDE +2A80; 2A7F # GREATER-THAN OR SLANTED EQUAL TO WITH DOT INSIDE +2A81; 2A82 # LESS-THAN OR SLANTED EQUAL TO WITH DOT ABOVE +2A82; 2A81 # GREATER-THAN OR SLANTED EQUAL TO WITH DOT ABOVE +2A83; 2A84 # LESS-THAN OR SLANTED EQUAL TO WITH DOT ABOVE RIGHT +2A84; 2A83 # GREATER-THAN OR SLANTED EQUAL TO WITH DOT ABOVE LEFT +2A8B; 2A8C # LESS-THAN ABOVE DOUBLE-LINE EQUAL ABOVE GREATER-THAN +2A8C; 2A8B # GREATER-THAN ABOVE DOUBLE-LINE EQUAL ABOVE LESS-THAN +2A91; 2A92 # LESS-THAN ABOVE GREATER-THAN ABOVE DOUBLE-LINE EQUAL +2A92; 2A91 # GREATER-THAN ABOVE LESS-THAN ABOVE DOUBLE-LINE EQUAL +2A93; 2A94 # LESS-THAN ABOVE SLANTED EQUAL ABOVE GREATER-THAN ABOVE SLANTED EQUAL +2A94; 2A93 # GREATER-THAN ABOVE SLANTED EQUAL ABOVE LESS-THAN ABOVE SLANTED EQUAL +2A95; 2A96 # SLANTED EQUAL TO OR LESS-THAN +2A96; 2A95 # SLANTED EQUAL TO OR GREATER-THAN +2A97; 2A98 # SLANTED EQUAL TO OR LESS-THAN WITH DOT INSIDE +2A98; 2A97 # SLANTED EQUAL TO OR GREATER-THAN WITH DOT INSIDE +2A99; 2A9A # DOUBLE-LINE EQUAL TO OR LESS-THAN +2A9A; 2A99 # DOUBLE-LINE EQUAL TO OR GREATER-THAN +2A9B; 2A9C # DOUBLE-LINE SLANTED EQUAL TO OR LESS-THAN +2A9C; 2A9B # DOUBLE-LINE SLANTED EQUAL TO OR GREATER-THAN +2AA1; 2AA2 # DOUBLE NESTED LESS-THAN +2AA2; 2AA1 # DOUBLE NESTED GREATER-THAN +2AA6; 2AA7 # LESS-THAN CLOSED BY CURVE +2AA7; 2AA6 # GREATER-THAN CLOSED BY CURVE +2AA8; 2AA9 # LESS-THAN CLOSED BY CURVE ABOVE SLANTED EQUAL +2AA9; 2AA8 # GREATER-THAN CLOSED BY CURVE ABOVE SLANTED EQUAL +2AAA; 2AAB # SMALLER THAN +2AAB; 2AAA # LARGER THAN +2AAC; 2AAD # SMALLER THAN OR EQUAL TO +2AAD; 2AAC # LARGER THAN OR EQUAL TO +2AAF; 2AB0 # PRECEDES ABOVE SINGLE-LINE EQUALS SIGN +2AB0; 2AAF # SUCCEEDS ABOVE SINGLE-LINE EQUALS SIGN +2AB3; 2AB4 # PRECEDES ABOVE EQUALS SIGN +2AB4; 2AB3 # SUCCEEDS ABOVE EQUALS SIGN +2ABB; 2ABC # DOUBLE PRECEDES +2ABC; 2ABB # DOUBLE SUCCEEDS +2ABD; 2ABE # SUBSET WITH DOT +2ABE; 2ABD # SUPERSET WITH DOT +2ABF; 2AC0 # SUBSET WITH PLUS SIGN BELOW +2AC0; 2ABF # SUPERSET WITH PLUS SIGN BELOW +2AC1; 2AC2 # SUBSET WITH MULTIPLICATION SIGN BELOW +2AC2; 2AC1 # SUPERSET WITH MULTIPLICATION SIGN BELOW +2AC3; 2AC4 # SUBSET OF OR EQUAL TO WITH DOT ABOVE +2AC4; 2AC3 # SUPERSET OF OR EQUAL TO WITH DOT ABOVE +2AC5; 2AC6 # SUBSET OF ABOVE EQUALS SIGN +2AC6; 2AC5 # SUPERSET OF ABOVE EQUALS SIGN +2ACD; 2ACE # SQUARE LEFT OPEN BOX OPERATOR +2ACE; 2ACD # SQUARE RIGHT OPEN BOX OPERATOR +2ACF; 2AD0 # CLOSED SUBSET +2AD0; 2ACF # CLOSED SUPERSET +2AD1; 2AD2 # CLOSED SUBSET OR EQUAL TO +2AD2; 2AD1 # CLOSED SUPERSET OR EQUAL TO +2AD3; 2AD4 # SUBSET ABOVE SUPERSET +2AD4; 2AD3 # SUPERSET ABOVE SUBSET +2AD5; 2AD6 # SUBSET ABOVE SUBSET +2AD6; 2AD5 # SUPERSET ABOVE SUPERSET +2ADE; 22A6 # SHORT LEFT TACK +2AE3; 22A9 # DOUBLE VERTICAL BAR LEFT TURNSTILE +2AE4; 22A8 # VERTICAL BAR DOUBLE LEFT TURNSTILE +2AE5; 22AB # DOUBLE VERTICAL BAR DOUBLE LEFT TURNSTILE +2AEC; 2AED # DOUBLE STROKE NOT SIGN +2AED; 2AEC # REVERSED DOUBLE STROKE NOT SIGN +2AF7; 2AF8 # TRIPLE NESTED LESS-THAN +2AF8; 2AF7 # TRIPLE NESTED GREATER-THAN +2AF9; 2AFA # DOUBLE-LINE SLANTED LESS-THAN OR EQUAL TO +2AFA; 2AF9 # DOUBLE-LINE SLANTED GREATER-THAN OR EQUAL TO +2E02; 2E03 # LEFT SUBSTITUTION BRACKET +2E03; 2E02 # RIGHT SUBSTITUTION BRACKET +2E04; 2E05 # LEFT DOTTED SUBSTITUTION BRACKET +2E05; 2E04 # RIGHT DOTTED SUBSTITUTION BRACKET +2E09; 2E0A # LEFT TRANSPOSITION BRACKET +2E0A; 2E09 # RIGHT TRANSPOSITION BRACKET +2E0C; 2E0D # LEFT RAISED OMISSION BRACKET +2E0D; 2E0C # RIGHT RAISED OMISSION BRACKET +2E1C; 2E1D # LEFT LOW PARAPHRASE BRACKET +2E1D; 2E1C # RIGHT LOW PARAPHRASE BRACKET +2E20; 2E21 # LEFT VERTICAL BAR WITH QUILL +2E21; 2E20 # RIGHT VERTICAL BAR WITH QUILL +2E22; 2E23 # TOP LEFT HALF BRACKET +2E23; 2E22 # TOP RIGHT HALF BRACKET +2E24; 2E25 # BOTTOM LEFT HALF BRACKET +2E25; 2E24 # BOTTOM RIGHT HALF BRACKET +2E26; 2E27 # LEFT SIDEWAYS U BRACKET +2E27; 2E26 # RIGHT SIDEWAYS U BRACKET +2E28; 2E29 # LEFT DOUBLE PARENTHESIS +2E29; 2E28 # RIGHT DOUBLE PARENTHESIS +3008; 3009 # LEFT ANGLE BRACKET +3009; 3008 # RIGHT ANGLE BRACKET +300A; 300B # LEFT DOUBLE ANGLE BRACKET +300B; 300A # RIGHT DOUBLE ANGLE BRACKET +300C; 300D # [BEST FIT] LEFT CORNER BRACKET +300D; 300C # [BEST FIT] RIGHT CORNER BRACKET +300E; 300F # [BEST FIT] LEFT WHITE CORNER BRACKET +300F; 300E # [BEST FIT] RIGHT WHITE CORNER BRACKET +3010; 3011 # LEFT BLACK LENTICULAR BRACKET +3011; 3010 # RIGHT BLACK LENTICULAR BRACKET +3014; 3015 # LEFT TORTOISE SHELL BRACKET +3015; 3014 # RIGHT TORTOISE SHELL BRACKET +3016; 3017 # LEFT WHITE LENTICULAR BRACKET +3017; 3016 # RIGHT WHITE LENTICULAR BRACKET +3018; 3019 # LEFT WHITE TORTOISE SHELL BRACKET +3019; 3018 # RIGHT WHITE TORTOISE SHELL BRACKET +301A; 301B # LEFT WHITE SQUARE BRACKET +301B; 301A # RIGHT WHITE SQUARE BRACKET +FE59; FE5A # SMALL LEFT PARENTHESIS +FE5A; FE59 # SMALL RIGHT PARENTHESIS +FE5B; FE5C # SMALL LEFT CURLY BRACKET +FE5C; FE5B # SMALL RIGHT CURLY BRACKET +FE5D; FE5E # SMALL LEFT TORTOISE SHELL BRACKET +FE5E; FE5D # SMALL RIGHT TORTOISE SHELL BRACKET +FE64; FE65 # SMALL LESS-THAN SIGN +FE65; FE64 # SMALL GREATER-THAN SIGN +FF08; FF09 # FULLWIDTH LEFT PARENTHESIS +FF09; FF08 # FULLWIDTH RIGHT PARENTHESIS +FF1C; FF1E # FULLWIDTH LESS-THAN SIGN +FF1E; FF1C # FULLWIDTH GREATER-THAN SIGN +FF3B; FF3D # FULLWIDTH LEFT SQUARE BRACKET +FF3D; FF3B # FULLWIDTH RIGHT SQUARE BRACKET +FF5B; FF5D # FULLWIDTH LEFT CURLY BRACKET +FF5D; FF5B # FULLWIDTH RIGHT CURLY BRACKET +FF5F; FF60 # FULLWIDTH LEFT WHITE PARENTHESIS +FF60; FF5F # FULLWIDTH RIGHT WHITE PARENTHESIS +FF62; FF63 # [BEST FIT] HALFWIDTH LEFT CORNER BRACKET +FF63; FF62 # [BEST FIT] HALFWIDTH RIGHT CORNER BRACKET + +# The following characters have no appropriate mirroring character. +# For these characters it is up to the rendering system +# to provide mirrored glyphs. + +# 2140; DOUBLE-STRUCK N-ARY SUMMATION +# 2201; COMPLEMENT +# 2202; PARTIAL DIFFERENTIAL +# 2203; THERE EXISTS +# 2204; THERE DOES NOT EXIST +# 2211; N-ARY SUMMATION +# 2216; SET MINUS +# 221A; SQUARE ROOT +# 221B; CUBE ROOT +# 221C; FOURTH ROOT +# 221D; PROPORTIONAL TO +# 221F; RIGHT ANGLE +# 2220; ANGLE +# 2221; MEASURED ANGLE +# 2222; SPHERICAL ANGLE +# 2224; DOES NOT DIVIDE +# 2226; NOT PARALLEL TO +# 222B; INTEGRAL +# 222C; DOUBLE INTEGRAL +# 222D; TRIPLE INTEGRAL +# 222E; CONTOUR INTEGRAL +# 222F; SURFACE INTEGRAL +# 2230; VOLUME INTEGRAL +# 2231; CLOCKWISE INTEGRAL +# 2232; CLOCKWISE CONTOUR INTEGRAL +# 2233; ANTICLOCKWISE CONTOUR INTEGRAL +# 2239; EXCESS +# 223B; HOMOTHETIC +# 223E; INVERTED LAZY S +# 223F; SINE WAVE +# 2240; WREATH PRODUCT +# 2241; NOT TILDE +# 2242; MINUS TILDE +# 2244; NOT ASYMPTOTICALLY EQUAL TO +# 2245; APPROXIMATELY EQUAL TO +# 2246; APPROXIMATELY BUT NOT ACTUALLY EQUAL TO +# 2247; NEITHER APPROXIMATELY NOR ACTUALLY EQUAL TO +# 2248; ALMOST EQUAL TO +# 2249; NOT ALMOST EQUAL TO +# 224A; ALMOST EQUAL OR EQUAL TO +# 224B; TRIPLE TILDE +# 224C; ALL EQUAL TO +# 225F; QUESTIONED EQUAL TO +# 2260; NOT EQUAL TO +# 2262; NOT IDENTICAL TO +# 228C; MULTISET +# 22A7; MODELS +# 22AA; TRIPLE VERTICAL BAR RIGHT TURNSTILE +# 22AC; DOES NOT PROVE +# 22AD; NOT TRUE +# 22AE; DOES NOT FORCE +# 22AF; NEGATED DOUBLE VERTICAL BAR DOUBLE RIGHT TURNSTILE +# 22B8; MULTIMAP +# 22BE; RIGHT ANGLE WITH ARC +# 22BF; RIGHT TRIANGLE +# 22F5; ELEMENT OF WITH DOT ABOVE +# 22F8; ELEMENT OF WITH UNDERBAR +# 22F9; ELEMENT OF WITH TWO HORIZONTAL STROKES +# 22FF; Z NOTATION BAG MEMBERSHIP +# 2320; TOP HALF INTEGRAL +# 2321; BOTTOM HALF INTEGRAL +# 27C0; THREE DIMENSIONAL ANGLE +# 27CC; LONG DIVISION +# 27D3; LOWER RIGHT CORNER WITH DOT +# 27D4; UPPER LEFT CORNER WITH DOT +# 27DC; LEFT MULTIMAP +# 299B; MEASURED ANGLE OPENING LEFT +# 299C; RIGHT ANGLE VARIANT WITH SQUARE +# 299D; MEASURED RIGHT ANGLE WITH DOT +# 299E; ANGLE WITH S INSIDE +# 299F; ACUTE ANGLE +# 29A0; SPHERICAL ANGLE OPENING LEFT +# 29A1; SPHERICAL ANGLE OPENING UP +# 29A2; TURNED ANGLE +# 29A3; REVERSED ANGLE +# 29A4; ANGLE WITH UNDERBAR +# 29A5; REVERSED ANGLE WITH UNDERBAR +# 29A6; OBLIQUE ANGLE OPENING UP +# 29A7; OBLIQUE ANGLE OPENING DOWN +# 29A8; MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING UP AND RIGHT +# 29A9; MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING UP AND LEFT +# 29AA; MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING DOWN AND RIGHT +# 29AB; MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING DOWN AND LEFT +# 29AC; MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING RIGHT AND UP +# 29AD; MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING LEFT AND UP +# 29AE; MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING RIGHT AND DOWN +# 29AF; MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING LEFT AND DOWN +# 29C2; CIRCLE WITH SMALL CIRCLE TO THE RIGHT +# 29C3; CIRCLE WITH TWO HORIZONTAL STROKES TO THE RIGHT +# 29C9; TWO JOINED SQUARES +# 29CE; RIGHT TRIANGLE ABOVE LEFT TRIANGLE +# 29DC; INCOMPLETE INFINITY +# 29E1; INCREASES AS +# 29E3; EQUALS SIGN AND SLANTED PARALLEL +# 29E4; EQUALS SIGN AND SLANTED PARALLEL WITH TILDE ABOVE +# 29E5; IDENTICAL TO AND SLANTED PARALLEL +# 29E8; DOWN-POINTING TRIANGLE WITH LEFT HALF BLACK +# 29E9; DOWN-POINTING TRIANGLE WITH RIGHT HALF BLACK +# 29F4; RULE-DELAYED +# 29F6; SOLIDUS WITH OVERBAR +# 29F7; REVERSE SOLIDUS WITH HORIZONTAL STROKE +# 2A0A; MODULO TWO SUM +# 2A0B; SUMMATION WITH INTEGRAL +# 2A0C; QUADRUPLE INTEGRAL OPERATOR +# 2A0D; FINITE PART INTEGRAL +# 2A0E; INTEGRAL WITH DOUBLE STROKE +# 2A0F; INTEGRAL AVERAGE WITH SLASH +# 2A10; CIRCULATION FUNCTION +# 2A11; ANTICLOCKWISE INTEGRATION +# 2A12; LINE INTEGRATION WITH RECTANGULAR PATH AROUND POLE +# 2A13; LINE INTEGRATION WITH SEMICIRCULAR PATH AROUND POLE +# 2A14; LINE INTEGRATION NOT INCLUDING THE POLE +# 2A15; INTEGRAL AROUND A POINT OPERATOR +# 2A16; QUATERNION INTEGRAL OPERATOR +# 2A17; INTEGRAL WITH LEFTWARDS ARROW WITH HOOK +# 2A18; INTEGRAL WITH TIMES SIGN +# 2A19; INTEGRAL WITH INTERSECTION +# 2A1A; INTEGRAL WITH UNION +# 2A1B; INTEGRAL WITH OVERBAR +# 2A1C; INTEGRAL WITH UNDERBAR +# 2A1E; LARGE LEFT TRIANGLE OPERATOR +# 2A1F; Z NOTATION SCHEMA COMPOSITION +# 2A20; Z NOTATION SCHEMA PIPING +# 2A21; Z NOTATION SCHEMA PROJECTION +# 2A24; PLUS SIGN WITH TILDE ABOVE +# 2A26; PLUS SIGN WITH TILDE BELOW +# 2A29; MINUS SIGN WITH COMMA ABOVE +# 2A3E; Z NOTATION RELATIONAL COMPOSITION +# 2A57; SLOPING LARGE OR +# 2A58; SLOPING LARGE AND +# 2A6A; TILDE OPERATOR WITH DOT ABOVE +# 2A6B; TILDE OPERATOR WITH RISING DOTS +# 2A6C; SIMILAR MINUS SIMILAR +# 2A6D; CONGRUENT WITH DOT ABOVE +# 2A6F; ALMOST EQUAL TO WITH CIRCUMFLEX ACCENT +# 2A70; APPROXIMATELY EQUAL OR EQUAL TO +# 2A73; EQUALS SIGN ABOVE TILDE OPERATOR +# 2A74; DOUBLE COLON EQUAL +# 2A7B; LESS-THAN WITH QUESTION MARK ABOVE +# 2A7C; GREATER-THAN WITH QUESTION MARK ABOVE +# 2A85; LESS-THAN OR APPROXIMATE +# 2A86; GREATER-THAN OR APPROXIMATE +# 2A87; LESS-THAN AND SINGLE-LINE NOT EQUAL TO +# 2A88; GREATER-THAN AND SINGLE-LINE NOT EQUAL TO +# 2A89; LESS-THAN AND NOT APPROXIMATE +# 2A8A; GREATER-THAN AND NOT APPROXIMATE +# 2A8D; LESS-THAN ABOVE SIMILAR OR EQUAL +# 2A8E; GREATER-THAN ABOVE SIMILAR OR EQUAL +# 2A8F; LESS-THAN ABOVE SIMILAR ABOVE GREATER-THAN +# 2A90; GREATER-THAN ABOVE SIMILAR ABOVE LESS-THAN +# 2A9D; SIMILAR OR LESS-THAN +# 2A9E; SIMILAR OR GREATER-THAN +# 2A9F; SIMILAR ABOVE LESS-THAN ABOVE EQUALS SIGN +# 2AA0; SIMILAR ABOVE GREATER-THAN ABOVE EQUALS SIGN +# 2AA3; DOUBLE NESTED LESS-THAN WITH UNDERBAR +# 2AB1; PRECEDES ABOVE SINGLE-LINE NOT EQUAL TO +# 2AB2; SUCCEEDS ABOVE SINGLE-LINE NOT EQUAL TO +# 2AB5; PRECEDES ABOVE NOT EQUAL TO +# 2AB6; SUCCEEDS ABOVE NOT EQUAL TO +# 2AB7; PRECEDES ABOVE ALMOST EQUAL TO +# 2AB8; SUCCEEDS ABOVE ALMOST EQUAL TO +# 2AB9; PRECEDES ABOVE NOT ALMOST EQUAL TO +# 2ABA; SUCCEEDS ABOVE NOT ALMOST EQUAL TO +# 2AC7; SUBSET OF ABOVE TILDE OPERATOR +# 2AC8; SUPERSET OF ABOVE TILDE OPERATOR +# 2AC9; SUBSET OF ABOVE ALMOST EQUAL TO +# 2ACA; SUPERSET OF ABOVE ALMOST EQUAL TO +# 2ACB; SUBSET OF ABOVE NOT EQUAL TO +# 2ACC; SUPERSET OF ABOVE NOT EQUAL TO +# 2ADC; FORKING +# 2AE2; VERTICAL BAR TRIPLE RIGHT TURNSTILE +# 2AE6; LONG DASH FROM LEFT MEMBER OF DOUBLE VERTICAL +# 2AEE; DOES NOT DIVIDE WITH REVERSED NEGATION SLASH +# 2AF3; PARALLEL WITH TILDE OPERATOR +# 2AFB; TRIPLE SOLIDUS BINARY RELATION +# 2AFD; DOUBLE SOLIDUS OPERATOR +# 1D6DB; MATHEMATICAL BOLD PARTIAL DIFFERENTIAL +# 1D715; MATHEMATICAL ITALIC PARTIAL DIFFERENTIAL +# 1D74F; MATHEMATICAL BOLD ITALIC PARTIAL DIFFERENTIAL +# 1D789; MATHEMATICAL SANS-SERIF BOLD PARTIAL DIFFERENTIAL +# 1D7C3; MATHEMATICAL SANS-SERIF BOLD ITALIC PARTIAL DIFFERENTIAL + +# EOF diff --git a/base/data/formats.sprop b/base/data/formats.sprop new file mode 100644 index 0000000..72f6e74 --- /dev/null +++ b/base/data/formats.sprop @@ -0,0 +1,160 @@ +# File storage for format strings + +################################################# +# Generic format strings for regular expressions. +################################################# + +## Format a regular expression for matching a delimiter separated list. +## Takes two parameters +## 1) The expression for each term +## 2) The expression for the delimiter +delimSeparatedList (?:%1$s(?:%2$s%1$s)*) + +###################################### +# CL format string regular expressions +###################################### + +## Format a regular expression for matching a potential CL format directive +## Has two parts +## 1) The optional set of prefix parameters +## 2) The optional modifier +## Captures three things +## 1) The prefix parameters +## 2) The modifiers +## 3) The directive name +## 4) The function name, if the directive was a function call. +clFormatDirective ~(?<params>%1$s)?(?<modifiers>%2$s?)(?:%3$s) + +#################################################### +# Format strings for handling double-quoted strings. +#################################################### + +## Format the three types of string escapes into a valid pattern. +## The three types are: +## 1) Short escapes. +## 2) Octal escapes. +## 3) Unicode escapes. +stringEscape \\(%1$s|%2$s|%3$s) + +## Format the parts of a regex into one that matches java-style double-quoted strings. +## The parts are: +## 1) Anything that's not a possible escape sequence or quote. +## 2) A possible escape sequence. +doubleQuotes ("(%1$s|%2$s)*") + +##################################### +# Format strings for handling doubles +##################################### + +## Format a floating point exponent regex. +## The parts are: +## 1) Exponent indicator, +## 2) One or more digits. +fpExponent %1$s%2$s + +## Format a decimal number with an integer part. +## The parts are: +## 1) A series of decimal digits +## 2) An exponent. +## +## The number format is: +## 1) An integer part +## 2) An optional dot +## 3) An optional decimal part +## 4) An optional exponent +fpDecimalInteger (?:%1$s(?:\.?)(?:%1$s?)(?:%2$s)?) + +## Format a decimal number with no integer part. +## The parts are: +## 1) A series of decimal digits +## 2) An exponent. +## +## The number format is: +## 1) A dot +## 2) A decimal part +## 3) An optional exponent +fpDecimalDecimal (?:\.(?:%1$s)(?:%2$s)?) + +## Format a hexadecimal number with no decimal part. +## The parts are: +## 1) A series of hex digits +## +## The number format is: +## 1) A hex leader. +## 2) A series of hex digits. +## 3) An optional dot. +fpHexInteger (?:0[xX]%1$s(?:\.)?) + +## Format a hexadecimal number with a decimal part +## The parts are: +## 1) A series of hex digits. +## +## The number format is: +## 1) A hex leader. +## 2) A optional series of hex digits. +## 3) A dot. +## 4) A series of hex digits. +fpHexDecimal (?:0[xX]%1$s?(?:\.)%1$s) + +## Format a hexadecimal leader before a prefix. +## The parts are: +## 1) A hex number with no decimal part +## 2) A hex number with a decimal part +fpHexLeader (?:%1$s|%2$s) + +## Format a hexadecimal floating point number. +## The parts are: +## 1) A hexadecimal leader. +## 2) A series of decimal digits. +## +## The number format is: +## 1) A hexadecimal leader. +## 2) A exponent indicator. +## 3) An optional sign. +## 4) A series of decimal digits. +fpHexString (?:%1$s[pP][+-]?%2$s) + +## Format the number part of a double. +## The parts are: +## 1) A decimal double with an integer part. +## 2) A decimal double without an integer part. +## 3) A hexadecimal double. +fpNumber (?:%1$s|%2$s|%3$s) + +## Format a floating point leader. +## +## NOTE: The other parts are completed by where we're inserted. + +## Format a double +## The parts are: +## 1) A leader +## 2) A number +## +## NOTE: The parens are not mismatched. +## The other one is contributed by the leader. +fpDouble %1$s(?:%2$s[fFdD]?))[\x00-\x20]* + +######################################### +# Format strings for handling delimiters. +######################################### + +## Format a raw delimiter +## The parts are +## 1) A regular expression +## +## This matches just the provided regular expression. +rawDelim (?:%1$s)| + +## Format a repeating delimiter +## The parts are +## 1) A string. +## +## This matches one or more occurances of the provided string as a literal. +multipleDelim (?:\Q%1$s\E)+| + +## Format a simple delimiter +## The parts are +## 1) A string. +## +## This matches one occurrence of the provided string as a literal. +simpleDelim (?:\Q%1$s\E)| diff --git a/base/data/regexes.sprop b/base/data/regexes.sprop new file mode 100644 index 0000000..89c5b4f --- /dev/null +++ b/base/data/regexes.sprop @@ -0,0 +1,61 @@ +# File storage for static regular expressions. + +######################################################## +# Regular expressions for handling double-quoted strings +######################################################## + +## Match a possible single character escape +possibleStringEscape \\. + +## Match valid string escapes +shortFormStringEscape [btnfr"'\\] +octalStringEscape [0-3]?[0-7]{1,2} +unicodeStringEscape u[0-9a-fA-F]{4} + +## Match an unescaped quote in a string. +unescapedQuote (?<!\\)\" + +## Match one or more characters that aren't part of an escape or a quote. +nonStringEscape [^\\\"]+ + +######################################## +# Double validation regular expressions. +######################################## + +## Unit pieces for doubles +fpDigits (?:\p{Digit}+) +fpHexDigits (?:\p{XDigit}+) + +## An exponent is e or E followed by a (optionally signed) decimal integer. +fpExponent [eE][+-]? + +## A double leader +## +## NOTE: The incomplete parts are finished by where it is inserted. +fpLeader [\x00-\x20]*[+-]?(?:NaN|Infinity| + +###################################### +# CL format string regular expressions +###################################### + +## Matches a format string prefix parameter +## A prefix parameter is one of +## * A signed decimal number +## * A single character preceded by a single quote +## * The letter V (or v) +## * The character # +clFormatPrefix (?:[-+]?\d+|'\S|[Vv]|#) + +## Match a format string modifier +## A modifier is either : or @, or both in either order +clFormatModifier (?:@|:|@:|:@) + +## Matches a directive name. +## A directive name is either +## 1) A single, non-whitespace, non-/ character +## 2) A name enclosed in /'s +clFormatName (?:(?<name>[\S&&[^/]])|(?:/(?<funcname>[\S&&[^/]]+)/)) +############################################## +# Miscellaneous validation regular expressions +############################################## +intLiteral \A[+\-]\d+\Z
\ No newline at end of file diff --git a/base/docs/man5/BlockReaderCLI.5 b/base/docs/man5/BlockReaderCLI.5 new file mode 100644 index 0000000..178b6de --- /dev/null +++ b/base/docs/man5/BlockReaderCLI.5 @@ -0,0 +1,8 @@ +.TH BlockReaderCLI 5 "2017-09-10" "" "" +.SH NAME +BlockReaderCLI \- configure \fBBlockReaders\fP from a script +.SH DESCRIPTION +A small language for configuring \fBBlockReaders\fP from a script. This is a +command based language, where each command comes on its own line for +configuration. Certain commands may start a sub-mode. +.SH diff --git a/base/pom.xml b/base/pom.xml new file mode 100644 index 0000000..222b248 --- /dev/null +++ b/base/pom.xml @@ -0,0 +1,87 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>bjc</groupId> + <artifactId>BJCUtils-Parent</artifactId> + <version>1.0.0</version> + </parent> + + <build> + <plugins> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <version>3.7.0</version> + <configuration> + <source>1.8</source> + <target>1.8</target> + </configuration> + </plugin> + </plugins> + <resources> + <resource> + <directory>data/</directory> + <includes> + <include>**/*.txt</include> + <include>**/*.sprop</include> + </includes> + </resource> + </resources> + </build> + + <groupId>bjc</groupId> + <artifactId>BJC-Utils2</artifactId> + <version>0.1.0-SNAPSHOT</version> + <packaging>jar</packaging> + + <name>BJC-Utils2</name> + <url>http://maven.apache.org</url> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <dependencies> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.12</version> + </dependency> + <dependency> + <groupId>org.junit.contrib</groupId> + <artifactId>junit-theories</artifactId> + <version>4.12</version> + </dependency> + <dependency> + <groupId>com.pholser</groupId> + <artifactId>junit-quickcheck-core</artifactId> + <version>0.5</version> + </dependency> + <dependency> + <groupId>com.pholser</groupId> + <artifactId>junit-quickcheck-generators</artifactId> + <version>0.5</version> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + <version>3.4</version> + </dependency> + <dependency> + <groupId>org.json</groupId> + <artifactId>json</artifactId> + <version>20160212</version> + </dependency> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + <version>21.0</version> + </dependency> + <dependency> + <groupId>com.ibm.icu</groupId> + <artifactId>icu4j</artifactId> + <version>58.2</version> + </dependency> + </dependencies> +</project> diff --git a/base/src/examples/java/bjc/utils/examples/AbbrevMapTest.java b/base/src/examples/java/bjc/utils/examples/AbbrevMapTest.java new file mode 100644 index 0000000..ac4ea76 --- /dev/null +++ b/base/src/examples/java/bjc/utils/examples/AbbrevMapTest.java @@ -0,0 +1,59 @@ +package bjc.utils.examples; + +import java.util.Scanner; + +import bjc.utils.esodata.AbbrevMap; +import bjc.utils.funcutils.StringUtils; + +/** + * Test for abbreviation map. + * + * @author EVE + * + */ +public class AbbrevMapTest { + /** + * Main method. + * + * @param args + * Unused CLI args. + */ + public static void main(final String[] args) { + final Scanner scn = new Scanner(System.in); + + final AbbrevMap map = new AbbrevMap(); + + System.out.print("Enter a command (blank line to quit): "); + String ln = scn.nextLine(); + + while (!ln.equals("")) { + final String[] commParts = ln.split(" "); + + switch (commParts[0]) { + case "add": + map.addWords(commParts[1]); + break; + case "remove": + map.removeWords(commParts[1]); + break; + case "recalc": + map.recalculate(); + break; + case "check": + final String list = StringUtils.toEnglishList(map.deabbrev(commParts[1]), false); + System.out.println(list); + break; + case "debug": + System.out.println(map.toString()); + break; + default: + System.out.println("Unknown command: " + ln); + } + + System.out.print("Enter a command (blank line to quit): "); + ln = scn.nextLine(); + } + + scn.close(); + } +} diff --git a/base/src/examples/java/bjc/utils/examples/BinarySearchTest.java b/base/src/examples/java/bjc/utils/examples/BinarySearchTest.java new file mode 100644 index 0000000..758af61 --- /dev/null +++ b/base/src/examples/java/bjc/utils/examples/BinarySearchTest.java @@ -0,0 +1,123 @@ +package bjc.utils.examples; + +import java.util.Scanner; + +import bjc.utils.funcdata.bst.BinarySearchTree; +import bjc.utils.funcdata.bst.TreeLinearizationMethod; + +/** + * Example showing how to use the binary search tree. + * + * @author ben + * + */ +public class BinarySearchTest { + private static void display(final BinarySearchTree<Character> tree, final Scanner input) { + System.out.print("What order would you like the tree to be printed in (m for options): "); + char command; + + while (true) { + command = input.nextLine().charAt(0); + TreeLinearizationMethod method = null; + + switch (command) { + case 'm': + System.out.println("Possible tree printing methods: "); + System.out.println("\tp: Preorder printing (print parent first, then left & right)."); + System.out.println("\ti: Inorder printing (print left first, then parent & right)."); + System.out.println("\to: Postorder printing (print left first, then right & parent)."); + break; + case 'p': + method = TreeLinearizationMethod.PREORDER; + break; + case 'i': + method = TreeLinearizationMethod.INORDER; + break; + case 'o': + method = TreeLinearizationMethod.POSTORDER; + break; + default: + System.out.println("ERROR: Unknown command."); + } + + if (method != null) { + tree.traverse(method, (element) -> { + System.out.println("Node: " + element); + return true; + }); + + return; + } + + System.out.print("What order would you like the tree to be printed in (m for options): "); + } + } + + /** + * Main method of class + * + * @param args + * Unused CLI args + */ + public static void main(final String[] args) { + final Scanner input = new Scanner(System.in); + System.out.println("Binary Tree Constructor/Searcher"); + final BinarySearchTree<Character> tree = new BinarySearchTree<>((o1, o2) -> o1 - o2); + + char command = ' '; + while (command != 'e') { + System.out.print("Enter a command (m for help): "); + command = input.nextLine().charAt(0); + + switch (command) { + case 'm': + System.out.println("Valid commands: "); + System.out.println("\tm: Display this help message."); + System.out.println("\te: Exit this program."); + System.out.println("\ta: Add a node to the binary tree."); + System.out.println("\td: Display the binary tree."); + System.out.println("\tr: Remove a node from the binary tree."); + System.out.println("\tf: Check if a given node is in the binary tree."); + System.out.println("\tt: Trim all deleted nodes from the tree."); + System.out.println("\tb: Balance the tree (also trims dead nodes)"); + break; + case 'a': + System.out.print("Enter the letter to add to the binary tree: "); + command = input.nextLine().charAt(0); + + tree.addNode(command); + break; + case 'r': + System.out.print("Enter the letter to add to the binary tree: "); + command = input.nextLine().charAt(0); + + tree.deleteNode(command); + break; + case 'd': + display(tree, input); + break; + case 'f': + System.out.print("Enter the letter to add to the binary tree: "); + command = input.nextLine().charAt(0); + + final boolean inTree = tree.isInTree(command); + if (inTree) { + System.out.printf("Node %s was found\n", command); + } else { + System.out.printf("Node %s was not found\n", command); + } + break; + case 't': + tree.trim(); + break; + case 'b': + tree.balance(); + break; + default: + System.out.println("ERROR: Unrecognized command."); + } + } + + input.close(); + } +} diff --git a/base/src/examples/java/bjc/utils/examples/DelimSplitterTest.java b/base/src/examples/java/bjc/utils/examples/DelimSplitterTest.java new file mode 100644 index 0000000..428f276 --- /dev/null +++ b/base/src/examples/java/bjc/utils/examples/DelimSplitterTest.java @@ -0,0 +1,504 @@ +package bjc.utils.examples; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Scanner; + +import bjc.utils.data.ITree; +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.IList; +import bjc.utils.funcutils.StringUtils; +import bjc.utils.parserutils.delims.DelimiterException; +import bjc.utils.parserutils.delims.DelimiterGroup; +import bjc.utils.parserutils.delims.RegexCloser; +import bjc.utils.parserutils.delims.RegexOpener; +import bjc.utils.parserutils.delims.SequenceDelimiter; +import bjc.utils.parserutils.delims.StringDelimiter; +import bjc.utils.parserutils.splitter.ConfigurableTokenSplitter; + +/** + * Test for {@link SequenceDelimiter} as well as + * {@link ConfigurableTokenSplitter} + * + * @author EVE + * + */ +public class DelimSplitterTest { + private ConfigurableTokenSplitter split; + + private StringDelimiter dlm; + + private Map<String, String> mirrored; + + private Map<String, DelimiterGroup<String>> groups; + + boolean verbose; + + /* + * Create a new tester. + */ + private DelimSplitterTest() { + loadMirrorDB(); + + groups = new HashMap<>(); + + split = new ConfigurableTokenSplitter(true); + + dlm = new StringDelimiter(); + + verbose = true; + } + + private void loadMirrorDB() { + mirrored = new HashMap<>(); + + final InputStream stream = getClass().getResourceAsStream("/BidiMirrorDB.txt"); + + try (Scanner scn = new Scanner(stream)) { + String ln = ""; + + while (scn.hasNextLine()) { + ln = scn.nextLine(); + + if (ln.equals("")) { + continue; + } + if (ln.startsWith("#")) { + continue; + } + + final int cp1 = Integer.parseInt(ln.substring(0, 4), 16); + final int cp2 = Integer.parseInt(ln.substring(6, 10), 16); + + final char[] cpa1 = Character.toChars(cp1); + final char[] cpa2 = Character.toChars(cp2); + + final String cps1 = new String(cpa1); + final String cps2 = new String(cpa2); + + mirrored.put(cps1, cps2); + } + } + } + + /* + * Run the tester interface. + */ + private void runLoop() { + final Scanner scn = new Scanner(System.in); + + System.out.print("Enter a command (blank line to quit): "); + String inp = scn.nextLine().trim(); + System.out.println(); + + while (!inp.equals("")) { + handleCommand(inp, scn, true); + + System.out.println(); + + System.out.print("Enter a command (blank line to quit): "); + inp = scn.nextLine(); + + System.out.println(); + } + + scn.close(); + } + + /* + * Handle a input command. + */ + private void handleCommand(final String inp, final Scanner scn, final boolean isInteractive) { + if (inp.equals("")) return; + + int idx = inp.indexOf(' '); + + if (idx == -1) { + idx = inp.length(); + } + + final String command = inp.substring(0, idx); + + final String args = inp.substring(idx).trim(); + final String[] argArray = args.split(" "); + + switch (command) { + case "test": + handleTest(args, false); + break; + case "test-ws": + handleTest(args, true); + break; + case "splitter-split": + handleSplit(argArray); + break; + case "splitter-compile": + split.compile(); + if (verbose) { + System.out.println("Compiled splitter"); + } + break; + case "splitter-add": + split.addSimpleDelimiters(argArray); + if (verbose) { + System.out.println("Added delimiters " + StringUtils.toEnglishList(argArray, true)); + } + break; + case "splitter-addmulti": + split.addMultiDelimiters(argArray); + if (verbose) { + System.out.println( + "Added multi-delimiters " + StringUtils.toEnglishList(argArray, true)); + } + break; + case "splitter-addmatch": + for (final String arg : argArray) { + split.addSimpleDelimiters(arg, mirrored.get(arg)); + } + if (verbose) { + System.out.println("Added matched delimiters " + + StringUtils.toEnglishList(argArray, true)); + } + break; + case "splitter-debug": + System.out.println(split.toString()); + break; + case "splitter-reset": + split = new ConfigurableTokenSplitter(true); + if (verbose) { + System.out.println("Reset splitter"); + } + break; + + case "delims-addgroup": + for (final String arg : argArray) { + dlm.addGroup(groups.get(arg)); + } + if (verbose) { + System.out.println("Added groups " + StringUtils.toEnglishList(argArray, true)); + } + break; + case "delims-setinitial": + dlm.setInitialGroup(groups.get(argArray[0])); + if (verbose) { + System.out.println("Set initial group"); + } + break; + case "delims-debug": + System.out.println(dlm.toString()); + break; + case "delims-test": + handleDelim(args); + break; + case "delims-reset": + dlm = new StringDelimiter(); + if (verbose) { + System.out.println("Reset delimiter"); + } + break; + case "delimgroups-new": + for (final String arg : argArray) { + groups.put(arg, new DelimiterGroup<>(arg)); + } + if (verbose) { + System.out.println("Created groups " + StringUtils.toEnglishList(argArray, true)); + } + break; + case "delimgroups-edit": + for (final String arg : argArray) { + handleEditGroup(arg, scn, isInteractive); + } + break; + case "delimgroups-debug": + for (final DelimiterGroup<String> group : groups.values()) { + System.out.println(group.toString()); + } + break; + case "delimgroups-reset": + dlm = new StringDelimiter(); + groups = new HashMap<>(); + if (verbose) { + System.out.println("Reset delimiter groups + delimiter"); + } + break; + case "load-file": + handleLoadFile(args); + break; + default: + System.out.println("Unknown command "); + } + + } + + /* + * Load script commands from a file. + */ + private void handleLoadFile(final String args) { + String pth = args; + + if (args.startsWith("\"")) { + pth = args.substring(1, args.length() - 1); + } + + try (FileInputStream fis = new FileInputStream(pth)) { + final Scanner scn = new Scanner(fis); + + while (scn.hasNextLine()) { + final String ln = scn.nextLine().trim(); + + if (ln.equals("")) { + continue; + } + if (ln.startsWith("#")) { + continue; + } + + if (verbose) { + System.out.println("\nRead command '" + ln + "' from file\n"); + } + handleCommand(ln, scn, false); + } + + scn.close(); + } catch (final FileNotFoundException fnfex) { + System.out.println("Couldn't find file '" + args + "'"); + } catch (final IOException ioex) { + System.out.println("I/O error with file '" + args + "'\nCause: " + ioex.getMessage()); + } + } + + /* + * Handle editing a group. + */ + private void handleEditGroup(final String arg, final Scanner scn, final boolean isInteractive) { + if (!groups.containsKey(arg)) { + System.out.println("No group named '" + arg + "'"); + return; + } + + final DelimiterGroup<String> group = groups.get(arg); + + if (verbose) { + System.out.println("Editing group '" + arg + "'"); + } + if (isInteractive) { + System.out.println("Enter command (blank line to stop editing): "); + } + + String ln = scn.nextLine().trim(); + + while (!ln.equals("")) { + int idx = ln.indexOf(' '); + + if (idx == -1) { + idx = ln.length(); + } + + final String command = ln.substring(0, idx); + + final String args = ln.substring(idx).trim(); + final String[] argArray = args.split(" "); + + switch (command) { + case "add-closing": + group.addClosing(argArray); + if (verbose) { + System.out.println( + "Added closers " + StringUtils.toEnglishList(argArray, true)); + } + break; + case "add-tlexclude": + group.addTopLevelForbid(argArray); + if (verbose) { + System.out.println("Added top-level exclusions " + + StringUtils.toEnglishList(argArray, true)); + } + break; + case "add-exclude": + group.addTopLevelForbid(argArray); + if (verbose) { + System.out.println("Added nested exclusions " + + StringUtils.toEnglishList(argArray, true)); + } + break; + case "add-subgroup": + group.addSubgroup(argArray[0], Integer.parseInt(argArray[1])); + if (verbose) { + System.out.printf("Added subgroup %s with priority %s\n", argArray[0], + argArray[1]); + } + break; + case "add-implied-subgroup": + group.implySubgroup(argArray[0], argArray[1]); + if (verbose) { + System.out.printf("Made closer '%s' imply a '%s' subgroup\n", argArray[0], + argArray[1]); + } + break; + case "add-opener": + group.addOpener(argArray[0], argArray[1]); + if (verbose) { + System.out.printf("Added opener '%s' for group '%s'\n", argArray[0], + argArray[1]); + } + break; + case "add-reopener": + group.addPredOpener(new RegexOpener(argArray[0], argArray[1])); + if (verbose) { + System.out.printf("Added regex '%s' as opener for '%s'\n", argArray[1], + argArray[0]); + } + break; + case "add-recloser": + group.addPredCloser(new RegexCloser(argArray[0])); + if (verbose) { + System.out.printf("Added parameterized string '%s' as closer\n", argArray[0]); + } + break; + case "debug": + System.out.println(group.toString()); + break; + default: + System.out.println("Unknown command " + command); + } + + if (isInteractive) { + System.out.println("Enter command (blank line to stop editing): "); + } + + ln = scn.nextLine().trim(); + } + + if (verbose) { + System.out.println("Finished editing group '" + arg + "'"); + } + } + + private void handleDelim(final String args) { + try { + final ITree<String> res = dlm.delimitSequence(args.split(" ")); + + printDelimSeq(res); + } catch (final DelimiterException dex) { + System.out.println("Expression '" + args + "' isn't properly delimited.\n\tCause: " + + dex.getMessage()); + } + } + + private void handleSplit(final String[] argArray) { + for (int i = 0; i < argArray.length; i++) { + final String arg = argArray[i]; + + final IList<String> strangs = split.split(arg); + + System.out.printf("%d '%s' %s\n", i, arg, strangs); + } + } + + private void handleTest(final String inp, final boolean splitWS) { + IList<String> strings; + + try { + strings = split.split(inp); + } catch (final IllegalStateException isex) { + System.out.println("Splitter must be compiled at least once before use."); + return; + } + + System.out.println("Split tokens: " + strings); + + if (splitWS) { + final List<String> tks = new LinkedList<>(); + + for (final String strang : strings) { + tks.addAll(Arrays.asList(strang.split(" "))); + } + + strings = new FunctionalList<>(tks); + } + try { + final ITree<String> delim = dlm.delimitSequence(strings.toArray(new String[0])); + + printDelimSeq(delim); + } catch (final DelimiterException dex) { + System.out.println("Expression isn't properly delimited."); + System.out.println("Cause: " + dex.getMessage()); + } + } + + private void printDelimSeq(final ITree<String> delim) { + System.out.println("Delimited tokens:\n" + delim.getChild(1).toString()); + System.out.print("Delimited expr: "); + printDelimTree(delim); + System.out.println(); + System.out.println(); + + System.out.println(); + } + + private void printDelimTree(final ITree<String> tree) { + final StringBuilder sb = new StringBuilder(); + + intPrintDelimTree(tree.getChild(1), sb); + + System.out.println(sb.toString().replaceAll("\\s+", " ")); + } + + private void intPrintDelimTree(final ITree<String> tree, final StringBuilder sb) { + tree.doForChildren((child) -> { + intPrintDelimNode(child, sb); + }); + } + + private void intPrintDelimNode(final ITree<String> tree, final StringBuilder sb) { + if (tree.getHead().equals("contents")) { + intPrintDelimTree(tree, sb); + return; + } + + switch (tree.getChildrenCount()) { + case 0: + sb.append(tree.getHead()); + sb.append(" "); + + break; + case 1: + intPrintDelimTree(tree.getChild(0), sb); + + break; + case 2: + intPrintDelimTree(tree.getChild(0).getChild(0), sb); + intPrintDelimNode(tree.getChild(1), sb); + + break; + case 3: + intPrintDelimNode(tree.getChild(0), sb); + + final ITree<String> contents = tree.getChild(1); + + intPrintDelimTree(contents.getChild(0), sb); + intPrintDelimNode(tree.getChild(2), sb); + + break; + } + } + + /** + * Main method + * + * @param args + * Unused CLI args. + */ + public static void main(final String[] args) { + final DelimSplitterTest tst = new DelimSplitterTest(); + + tst.runLoop(); + } +} diff --git a/base/src/examples/java/bjc/utils/examples/ShuntTest.java b/base/src/examples/java/bjc/utils/examples/ShuntTest.java new file mode 100644 index 0000000..ed530ed --- /dev/null +++ b/base/src/examples/java/bjc/utils/examples/ShuntTest.java @@ -0,0 +1,37 @@ +package bjc.utils.examples; + +import java.util.Scanner; + +import bjc.utils.funcdata.FunctionalStringTokenizer; +import bjc.utils.funcdata.IList; +import bjc.utils.parserutils.ShuntingYard; + +/** + * Test of shunting yard + * + * @author ben + * + */ +public class ShuntTest { + /** + * Main method + * + * @param args + * Unused CLI args + */ + public static void main(final String[] args) { + final Scanner inputSource = new Scanner(System.in); + + System.out.print("Enter a expression to shunt: "); + final String line = inputSource.nextLine(); + + final ShuntingYard<String> yard = new ShuntingYard<>(true); + + final IList<String> preTokens = new FunctionalStringTokenizer(line).toList(strang -> strang); + final IList<String> shuntedTokens = yard.postfix(preTokens, strang -> strang); + + System.out.println(shuntedTokens.toString()); + + inputSource.close(); + } +} diff --git a/base/src/examples/java/bjc/utils/examples/rangen/DiabloItemGen.java b/base/src/examples/java/bjc/utils/examples/rangen/DiabloItemGen.java new file mode 100644 index 0000000..250318f --- /dev/null +++ b/base/src/examples/java/bjc/utils/examples/rangen/DiabloItemGen.java @@ -0,0 +1,94 @@ +package bjc.utils.examples.rangen; + +import bjc.utils.funcdata.FunctionalStringTokenizer; +import bjc.utils.funcdata.IList; +import bjc.utils.gen.WeightedGrammar; + +/** + * Example showing how to use the weighted random number generator. + * + * @author ben + * + */ +public class DiabloItemGen { + private static WeightedGrammar<String> rules = new WeightedGrammar<>(); + + private static void addCase(final String ruleName, final int probability, final String ruleParts) { + final IList<String> parts = FunctionalStringTokenizer.fromString(ruleParts).toList(strang -> strang); + + rules.addCase(ruleName, probability, parts); + } + + private static void addInfixRules() { + final String rn = "<infix>"; + + addCase(rn, 60, "sword"); + addCase(rn, 50, "armor"); + addCase(rn, 40, "rune"); + addCase(rn, 30, "scroll"); + addCase(rn, 20, "potion"); + addCase(rn, 10, "helm"); + } + + private static void addItemRules() { + final String rn = "<item>"; + + addCase(rn, 10, "<infix>"); + addCase(rn, 20, "<prefix> <infix>"); + addCase(rn, 30, "<infix> <suffix>"); + addCase(rn, 40, "<prefix> <infix> <suffix>"); + addCase(rn, 50, "<prefix> <prefix> <infix>"); + addCase(rn, 60, "<prefix> <prefix> <infix> <suffix>"); + } + + private static void addPrefixRules() { + final String rn = "<prefix>"; + + addCase(rn, 60, "sturdy"); + addCase(rn, 50, "fine"); + addCase(rn, 40, "strong"); + addCase(rn, 30, "azure"); + addCase(rn, 20, "crimson"); + addCase(rn, 10, "phasing"); + } + + private static void addSuffixRules() { + final String rn = "<suffix>"; + + addCase(rn, 60, "of Health"); + addCase(rn, 50, "of Wealth"); + addCase(rn, 40, "of Life"); + addCase(rn, 30, "of the Jackal"); + addCase(rn, 20, "of Vitality"); + addCase(rn, 10, "of Ability"); + } + + /** + * Main Method + * + * @param args + * Unused CLI args + */ + public static void main(final String[] args) { + rules.addRule("<item>"); + addItemRules(); + + rules.addRule("<suffix>"); + addSuffixRules(); + + rules.addRule("<prefix>"); + addPrefixRules(); + + rules.addRule("<infix>"); + addInfixRules(); + + for (int i = 0; i < 100; i++) { + final IList<String> ls = rules.generateListValues("<item>", " "); + + final StringBuilder sb = new StringBuilder(); + ls.forEach(sb::append); + + System.out.println(sb.toString().replaceAll("\\s+", " ")); + } + } +} diff --git a/base/src/examples/java/bjc/utils/examples/rangen/RandomStringExamples.java b/base/src/examples/java/bjc/utils/examples/rangen/RandomStringExamples.java new file mode 100644 index 0000000..a84f70d --- /dev/null +++ b/base/src/examples/java/bjc/utils/examples/rangen/RandomStringExamples.java @@ -0,0 +1,64 @@ +package bjc.utils.examples.rangen; + +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.FunctionalStringTokenizer; +import bjc.utils.funcdata.IList; +import bjc.utils.gen.RandomGrammar; + +/** + * Examples of random grammar + * + * @author ben + * + */ +public class RandomStringExamples { + private static RandomGrammar<String> rg; + + private static void addRule(final String rule, final String... cases) { + final IList<IList<String>> cses = new FunctionalList<>(); + + for (final String strang : cases) { + final IList<String> lst = FunctionalStringTokenizer.fromString(strang).toList(s -> s); + + cses.add(lst); + } + + rg.makeRule(rule, cses); + } + + /** + * Main method + * + * @param args + * Unused CLI args + */ + public static void main(final String[] args) { + rg = new RandomGrammar<>(); + + addRule("<sentance>", "<person> <opines> <something>", "<person> thinks that I am <property>", + "I <opine> <something>", "You think that I am <property>"); + + addRule("<activity>", "dancing", "eating", "sleeping"); + + addRule("<object>", "<person>", "life", "my computer", "my friends"); + + addRule("<opine>", "hate", "am jealous of", "love"); + + addRule("<opines>", "hates", "loves"); + + addRule("<person>", "my sister", "my father", "my girlfriend", "the man next door"); + + addRule("<property>", "creative", "intelligent"); + + addRule("<something>", "<activity>", "<activity> with <person>", "<object>"); + + for (int i = 0; i < 10; i++) { + final IList<String> ls = rg.generateListValues("<sentance>", " "); + + final StringBuilder sb = new StringBuilder(); + ls.forEach(sb::append); + + System.out.println(sb.toString().replaceAll("\\s+", " ")); + } + } +} diff --git a/base/src/examples/java/bjc/utils/examples/sample-ds-files/html.ds b/base/src/examples/java/bjc/utils/examples/sample-ds-files/html.ds new file mode 100644 index 0000000..103fa12 --- /dev/null +++ b/base/src/examples/java/bjc/utils/examples/sample-ds-files/html.ds @@ -0,0 +1,10 @@ +delimgroups-new tag initial + +delimgroups-edit tag + add-recloser </%2$s> + +delimgroups-edit initial + add-reopener tag <(\w+)> + +delims-addgroup tag initial +delims-setinitial initial
\ No newline at end of file diff --git a/base/src/examples/java/bjc/utils/examples/sample-ds-files/json.ds b/base/src/examples/java/bjc/utils/examples/sample-ds-files/json.ds new file mode 100644 index 0000000..d110d95 --- /dev/null +++ b/base/src/examples/java/bjc/utils/examples/sample-ds-files/json.ds @@ -0,0 +1,23 @@ +splitter-addmatch ( { [ +splitter-add : , " +splitter-compile + +delimgroups-new braces brackets initial +delimgroups-edit braces + add-closing } + add-subgroup : 0 + add-subgroup , 1 + add-implied-subgroup } , + +delimgroups-edit brackets + add-subgroup , 0 + add-closing ] + add-implied-subgroup ] , + +delimgroups-edit initial + add-opener { braces + add-opener [ brackets + +delims-addgroup braces brackets initial + +delims-setinitial initial
\ No newline at end of file diff --git a/base/src/examples/java/bjc/utils/examples/test.tree b/base/src/examples/java/bjc/utils/examples/test.tree new file mode 100644 index 0000000..795cc88 --- /dev/null +++ b/base/src/examples/java/bjc/utils/examples/test.tree @@ -0,0 +1,13 @@ +test 1 + 1 + 1 + 2 + 2 + 1 + +simp 1 + 2 + 3 + 4 + 3 + 2
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/PropertyDB.java b/base/src/main/java/bjc/utils/PropertyDB.java new file mode 100644 index 0000000..713e1e0 --- /dev/null +++ b/base/src/main/java/bjc/utils/PropertyDB.java @@ -0,0 +1,160 @@ +package bjc.utils; + +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.regex.Pattern; + +import bjc.utils.funcutils.LambdaLock; +import bjc.utils.ioutils.SimpleProperties; + +/** + * Database for storage of properties from external files. + * + * @author EVE + * + */ +public class PropertyDB { + private static SimpleProperties regexes; + private static Map<String, Pattern> compiledRegexes; + + private static SimpleProperties formats; + + /* + * Whether or not to log during the loading. + */ + private static final boolean LOGLOAD = false; + + /* + * The lock to use to ensure a read can't happen during a reload + */ + private static LambdaLock loadLock = new LambdaLock(); + + static { + reloadProperties(); + } + + /** + * Reload all the properties from their files. + * + * NOTE: Any attempts to read from the property DB while properties are + * being loaded will block, to prevent reads from partial states. + */ + public static void reloadProperties() { + /* + * Do the load with the write lock taken. + */ + loadLock.write(() -> { + if (LOGLOAD) { + System.out.println("Reading regex properties:"); + } + + /* + * Load regexes. + */ + regexes = new SimpleProperties(); + regexes.loadFrom(PropertyDB.class.getResourceAsStream("/regexes.sprop"), false); + if (LOGLOAD) { + regexes.outputProperties(); + System.out.println(); + } + compiledRegexes = new HashMap<>(); + + if (LOGLOAD) { + System.out.println("Reading format properties:"); + } + + /* + * Load formats. + */ + formats = new SimpleProperties(); + formats.loadFrom(PropertyDB.class.getResourceAsStream("/formats.sprop"), false); + if (LOGLOAD) { + formats.outputProperties(); + System.out.println(); + } + }); + } + + /** + * Retrieve a persisted regular expression. + * + * @param key + * The name of the regular expression. + * + * @return The regular expression with that name. + */ + public static String getRegex(final String key) { + return loadLock.read(() -> { + if (!regexes.containsKey(key)) { + final String msg = String.format("No regular expression named '%s' found", key); + + throw new NoSuchElementException(msg); + } + + return regexes.get(key); + }); + } + + /** + * Retrieve a persisted regular expression, compiled into a regular + * expression. + * + * @param key + * The name of the regular expression. + * + * @return The regular expression with that name. + */ + public static Pattern getCompiledRegex(final String key) { + return loadLock.read(() -> { + if (!regexes.containsKey(key)) { + final String msg = String.format("No regular expression named '%s' found", key); + + throw new NoSuchElementException(msg); + } + + /* + * Get the regex, and cache a compiled version. + */ + return compiledRegexes.computeIfAbsent(key, strang -> { + return Pattern.compile(regexes.get(strang)); + }); + }); + } + + /** + * Retrieve a persisted format string. + * + * @param key + * The name of the format string. + * + * @return The format string with that name. + */ + public static String getFormat(final String key) { + return loadLock.read(() -> { + if (!formats.containsKey(key)) { + final String msg = String.format("No format string named '%s' found", key); + + throw new NoSuchElementException(msg); + } + + return formats.get(key); + }); + } + + /** + * Retrieve a persisted format string, and apply it to a set of + * arguments. + * + * @param key + * The name of the format string. + * + * @param objects + * The parameters to the format string. + * + * @return The format string with that name. + */ + public static String applyFormat(final String key, final Object... objects) { + return String.format(getFormat(key), objects); + } +} diff --git a/base/src/main/java/bjc/utils/cli/CLICommander.java b/base/src/main/java/bjc/utils/cli/CLICommander.java new file mode 100644 index 0000000..cccb255 --- /dev/null +++ b/base/src/main/java/bjc/utils/cli/CLICommander.java @@ -0,0 +1,134 @@ +package bjc.utils.cli; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.Arrays; +import java.util.Scanner; + +/** + * Runs a CLI interface from the provided set of streams. + * + * @author ben + * + */ +public class CLICommander { + /* + * The streams used for input and normal/error output. + */ + private final InputStream input; + private final OutputStream output; + private final OutputStream error; + + /* + * The command mode to start execution in. + */ + private CommandMode initialMode; + + /** + * Create a new CLI interface powered by streams. + * + * @param input + * The stream to get user input from. + * @param output + * The stream to send normal output to. + * @param error + * The stream to send error output to. + */ + public CLICommander(final InputStream input, final OutputStream output, final OutputStream error) { + if (input == null) + throw new NullPointerException("Input stream must not be null"); + else if (output == null) + throw new NullPointerException("Output stream must not be null"); + else if (error == null) throw new NullPointerException("Error stream must not be null"); + + this.input = input; + this.output = output; + this.error = error; + } + + /** + * Start handling commands from the given input stream. + */ + public void runCommands() { + /* + * Setup output streams. + */ + final PrintStream normalOutput = new PrintStream(output); + final PrintStream errorOutput = new PrintStream(error); + + /* + * Set up input streams. + * + * We're suppressing the warning because we might use the input + * stream multiple times. + */ + @SuppressWarnings("resource") + final Scanner inputSource = new Scanner(input); + + /* + * The mode currently being used to handle commands. + * + * Used to preserve the initial mode. + */ + CommandMode currentMode = initialMode; + + /* + * Process commands until we're told to stop. + */ + while (currentMode != null) { + /* + * Print out the command prompt, using a custom prompt + * if one is specified. + */ + if (currentMode.isCustomPromptEnabled()) { + normalOutput.print(currentMode.getCustomPrompt()); + } else { + normalOutput.print(currentMode.getName() + ">> "); + } + + /* + * Read in a command. + */ + final String currentLine = inputSource.nextLine(); + + /* + * Handle commands we can handle. + */ + if (currentMode.canHandle(currentLine)) { + final String[] commandTokens = currentLine.split(" "); + String[] commandArgs = null; + + final int argCount = commandTokens.length; + + /* + * Parse args if they are present. + */ + if (argCount > 1) { + commandArgs = Arrays.copyOfRange(commandTokens, 1, argCount); + } + + /* + * Process command. + */ + currentMode = currentMode.process(commandTokens[0], commandArgs); + } else { + errorOutput.print("Error: Unrecognized command " + currentLine); + } + } + + normalOutput.print("Exiting now."); + } + + /** + * Set the initial command mode to use. + * + * @param initialMode + * The initial command mode to use. + */ + public void setInitialCommandMode(final CommandMode initialMode) { + if (initialMode == null) throw new NullPointerException("Initial mode must be non-zero"); + + this.initialMode = initialMode; + } +} diff --git a/base/src/main/java/bjc/utils/cli/Command.java b/base/src/main/java/bjc/utils/cli/Command.java new file mode 100644 index 0000000..02bc061 --- /dev/null +++ b/base/src/main/java/bjc/utils/cli/Command.java @@ -0,0 +1,39 @@ +package bjc.utils.cli; + +/** + * Represents a command that can be invoked from a {@link CommandMode} + * + * @author ben + * + */ +public interface Command { + /** + * Create a command that serves as an alias to this one + * + * @return A command that serves as an alias to this one + */ + Command aliased(); + + /** + * Get the handler that executes this command + * + * @return The handler that executes this command + */ + CommandHandler getHandler(); + + /** + * Get the help entry for this command + * + * @return The help entry for this command + */ + CommandHelp getHelp(); + + /** + * Check if this command is an alias of another command + * + * @return Whether or not this command is an alias of another + */ + default boolean isAlias() { + return false; + } +} diff --git a/base/src/main/java/bjc/utils/cli/CommandHandler.java b/base/src/main/java/bjc/utils/cli/CommandHandler.java new file mode 100644 index 0000000..2548248 --- /dev/null +++ b/base/src/main/java/bjc/utils/cli/CommandHandler.java @@ -0,0 +1,24 @@ +package bjc.utils.cli; + +import java.util.function.Function; + +/** + * A handler for a command + * + * @author ben + * + */ +@FunctionalInterface +public interface CommandHandler extends Function<String[], CommandMode> { + /** + * Execute this command + * + * @param args + * The arguments for this command + * @return The command mode to switch to after this command, or null to + * stop executing commands + */ + default CommandMode handle(final String[] args) { + return this.apply(args); + } +} diff --git a/base/src/main/java/bjc/utils/cli/CommandHelp.java b/base/src/main/java/bjc/utils/cli/CommandHelp.java new file mode 100644 index 0000000..86567a0 --- /dev/null +++ b/base/src/main/java/bjc/utils/cli/CommandHelp.java @@ -0,0 +1,31 @@ +package bjc.utils.cli; + +/** + * Interface for the help entry for a command + * + * @author ben + * + */ +public interface CommandHelp { + /** + * Get the description of a command. + * + * @return The description of a command + */ + String getDescription(); + + /** + * Get the summary line for a command. + * + * A summary line should consist of a string of the following format + * + * <pre> + * "<command-name>\t<command-summary>" + * </pre> + * + * where anything in angle brackets should be filled in. + * + * @return The summary line line for a command + */ + String getSummary(); +} diff --git a/base/src/main/java/bjc/utils/cli/CommandMode.java b/base/src/main/java/bjc/utils/cli/CommandMode.java new file mode 100644 index 0000000..39c72fc --- /dev/null +++ b/base/src/main/java/bjc/utils/cli/CommandMode.java @@ -0,0 +1,72 @@ +package bjc.utils.cli; + +/** + * A mode for determining the commands that are valid to enter, and then + * handling those commands + * + * @author ben + * + */ +public interface CommandMode extends Comparable<CommandMode> { + /** + * Check to see if this mode can handle the specified command + * + * @param command + * The command to check + * @return Whether or not this mode can handle the command. It is + * assumed not by default + */ + default boolean canHandle(final String command) { + return false; + }; + + /** + * Get the custom prompt for this mode + * + * @return the custom prompt for this mode + * + * @throws UnsupportedOperationException + * if this mode doesn't support a custom prompt + */ + default String getCustomPrompt() { + throw new UnsupportedOperationException("This mode doesn't support a custom prompt"); + } + + /** + * Get the name of this command mode + * + * @return The name of this command mode, which is the empty string by + * default + */ + public default String getName() { + return ""; + } + + /** + * Check if this mode uses a custom prompt + * + * @return Whether or not this mode uses a custom prompt + */ + default boolean isCustomPromptEnabled() { + return false; + } + + /** + * Process a command in this mode + * + * @param command + * The command to process + * @param args + * A list of arguments to the command + * @return The command mode to use for the next command. Defaults to + * returning this, and doing nothing else + */ + default CommandMode process(final String command, final String[] args) { + return this; + } + + @Override + default int compareTo(final CommandMode o) { + return getName().compareTo(o.getName()); + } +} diff --git a/base/src/main/java/bjc/utils/cli/DelegatingCommand.java b/base/src/main/java/bjc/utils/cli/DelegatingCommand.java new file mode 100644 index 0000000..acaa3a6 --- /dev/null +++ b/base/src/main/java/bjc/utils/cli/DelegatingCommand.java @@ -0,0 +1,64 @@ +package bjc.utils.cli; + +/** + * A class for a command that delegates to another command. + * + * @author ben + * + */ +class DelegatingCommand implements Command { + /* + * The command to delegate to. + */ + private final Command delegate; + + /** + * Create a new command that delegates to another command. + * + * @param delegate + * The command to delegate to. + */ + public DelegatingCommand(final Command delegate) { + this.delegate = delegate; + } + + @Override + public Command aliased() { + return new DelegatingCommand(delegate); + } + + @Override + public CommandHandler getHandler() { + return delegate.getHandler(); + } + + @Override + public CommandHelp getHelp() { + return delegate.getHelp(); + } + + @Override + public boolean isAlias() { + return true; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("DelegatingCommand ["); + + if (delegate != null) { + builder.append("delegate="); + builder.append(delegate); + } + + builder.append("]"); + + return builder.toString(); + } +} diff --git a/base/src/main/java/bjc/utils/cli/GenericCommand.java b/base/src/main/java/bjc/utils/cli/GenericCommand.java new file mode 100644 index 0000000..4ae4dea --- /dev/null +++ b/base/src/main/java/bjc/utils/cli/GenericCommand.java @@ -0,0 +1,84 @@ +package bjc.utils.cli; + +/** + * Generic command implementation. + * + * @author ben + * + */ +public class GenericCommand implements Command { + /* + * The behavior for invoking the command. + */ + private final CommandHandler handler; + + /* + * The help for the command. + */ + private CommandHelp help; + + /** + * Create a new generic command. + * + * @param handler + * The handler to use for the command. + * @param description + * The description of the command. May be null, in which + * case a default is provided. + * @param help + * The detailed help message for the command. May be + * null, in which case the description is repeated for + * the detailed help. + */ + public GenericCommand(final CommandHandler handler, final String description, final String help) { + if (handler == null) throw new NullPointerException("Command handler must not be null"); + + this.handler = handler; + + if (description == null) { + this.help = new NullHelp(); + } else { + this.help = new GenericHelp(description, help); + } + } + + @Override + public Command aliased() { + return new DelegatingCommand(this); + } + + @Override + public CommandHandler getHandler() { + return handler; + } + + @Override + public CommandHelp getHelp() { + return help; + } + + @Override + public boolean isAlias() { + return false; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("GenericCommand ["); + + if (help != null) { + builder.append("help="); + builder.append(help); + } + + builder.append("]"); + + return builder.toString(); + } +} diff --git a/base/src/main/java/bjc/utils/cli/GenericCommandMode.java b/base/src/main/java/bjc/utils/cli/GenericCommandMode.java new file mode 100644 index 0000000..8764537 --- /dev/null +++ b/base/src/main/java/bjc/utils/cli/GenericCommandMode.java @@ -0,0 +1,469 @@ +package bjc.utils.cli; + +import java.util.TreeMap; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import bjc.utils.funcdata.FunctionalMap; +import bjc.utils.funcdata.IMap; + +/** + * A general command mode, with a customizable set of commands + * + * There is a small set of commands which is handled by default. The first is + * 'list', which lists all the commands the user can input. The second is + * 'alias', which allows the user to bind a new name to a command + * + * @author ben + * + */ +public class GenericCommandMode implements CommandMode { + /* + * Contains the commands this mode handles + */ + private final IMap<String, Command> commandHandlers; + private final IMap<String, Command> defaultHandlers; + + /* + * Contains help topics without an associated command + */ + private final IMap<String, CommandHelp> helpTopics; + + /* + * The action to execute upon encountering an unknown command + */ + private BiConsumer<String, String[]> unknownCommandHandler; + + /* + * The functions to use for input/output + */ + private final Consumer<String> errorOutput; + private final Consumer<String> normalOutput; + + /* + * The name of this command mode, or null if it is unnamed + */ + private String modeName; + + /* + * The custom prompt to use, or null if none is specified + */ + private String customPrompt; + + /** + * Create a new generic command mode + * + * @param normalOutput + * The function to use for normal output + * @param errorOutput + * The function to use for error output + */ + public GenericCommandMode(final Consumer<String> normalOutput, final Consumer<String> errorOutput) { + if (normalOutput == null) + throw new NullPointerException("Normal output source must be non-null"); + else if (errorOutput == null) throw new NullPointerException("Error output source must be non-null"); + + this.normalOutput = normalOutput; + this.errorOutput = errorOutput; + + /* + * Initialize handler maps so that they sort in alphabetical + */ + /* + * order + */ + commandHandlers = new FunctionalMap<>(new TreeMap<>()); + defaultHandlers = new FunctionalMap<>(new TreeMap<>()); + helpTopics = new FunctionalMap<>(new TreeMap<>()); + + setupDefaultCommands(); + } + + /** + * Add an alias to an existing command + * + * @param commandName + * The name of the command to add an alias for + * @param aliasName + * The new alias for the command + * + * @throws IllegalArgumentException + * if the specified command doesn't have a bound + * handler, or if the alias name already has a bound + * value + */ + public void addCommandAlias(final String commandName, final String aliasName) { + if (commandName == null) + throw new NullPointerException("Command name must not be null"); + else if (aliasName == null) + throw new NullPointerException("Alias name must not be null"); + else if (!commandHandlers.containsKey(commandName) && !defaultHandlers.containsKey(commandName)) + throw new IllegalArgumentException("Cannot alias non-existant command '" + commandName + "'"); + else if (commandHandlers.containsKey(aliasName) || defaultHandlers.containsKey(aliasName)) + throw new IllegalArgumentException( + "Cannot bind alias '" + aliasName + "' to a command with a bound handler"); + else { + Command aliasedCommand; + + if (defaultHandlers.containsKey(commandName)) { + aliasedCommand = defaultHandlers.get(commandName).aliased(); + } else { + aliasedCommand = commandHandlers.get(commandName).aliased(); + } + + commandHandlers.put(aliasName, aliasedCommand); + } + } + + /** + * Add a command to this command mode + * + * @param command + * The name of the command to add + * @param handler + * The handler to use for the specified command + * + * @throws IllegalArgumentException + * if the specified command already has a handler + * registered + */ + public void addCommandHandler(final String command, final Command handler) { + if (command == null) + throw new NullPointerException("Command must not be null"); + else if (handler == null) + throw new NullPointerException("Handler must not be null"); + else if (canHandle(command)) + throw new IllegalArgumentException("Command " + command + " already has a handler registered"); + else { + commandHandlers.put(command, handler); + } + } + + /** + * Add a help topic to this command mode that isn't tied to a command + * + * @param topicName + * The name of the topic + * @param topic + * The contents of the topic + */ + public void addHelpTopic(final String topicName, final CommandHelp topic) { + helpTopics.put(topicName, topic); + } + + /* + * Default command builders + */ + + private GenericCommand buildAliasCommand() { + final String aliasShortHelp = "alias\tAlias one command to another"; + final String aliasLongHelp = "Gives a command another name it can be invoked by." + + " Invoke with two arguments: the name of the command to alias" + + "followed by the name of the alias to give that command."; + + return new GenericCommand((args) -> { + doAliasCommands(args); + + return this; + }, aliasShortHelp, aliasLongHelp); + } + + private GenericCommand buildClearCommands() { + final String clearShortHelp = "clear\tClear the screen"; + final String clearLongHelp = "Clears the screen of all the text on it," + " and prints a new prompt."; + + return new GenericCommand((args) -> { + errorOutput.accept("ERROR: This console doesn't support screen clearing"); + + return this; + }, clearShortHelp, clearLongHelp); + } + + private GenericCommand buildExitCommand() { + final String exitShortHelp = "exit\tExit the console"; + final String exitLongHelp = "First prompts the user to make sure they want to" + + " exit, then quits if they say they do"; + + return new GenericCommand((args) -> { + errorOutput.accept("ERROR: This console doesn't support auto-exiting"); + + return this; + }, exitShortHelp, exitLongHelp); + } + + private GenericCommand buildHelpCommand() { + final String helpShortHelp = "help\tConsult the help system"; + final String helpLongHelp = "Consults the internal help system." + + " Invoked in two different ways. Invoking with no arguments" + + " causes all the topics you can ask for details on to be list," + + " while invoking with the name of a topic will print the entry" + " for that topic"; + + return new GenericCommand((args) -> { + if (args == null || args.length == 0) { + /* + * Invoke general help + */ + doHelpSummary(); + } else { + /* + * Invoke help for a command + */ + doHelpCommand(args[0]); + } + + return this; + }, helpShortHelp, helpLongHelp); + } + + private GenericCommand buildListCommand() { + final String listShortHelp = "list\tList available commands"; + final String listLongHelp = "Lists all of the commands available in this mode," + + " as well as commands available in any mode"; + + return new GenericCommand((args) -> { + doListCommands(); + + return this; + }, listShortHelp, listLongHelp); + } + + @Override + public boolean canHandle(final String command) { + return commandHandlers.containsKey(command) || defaultHandlers.containsKey(command); + } + + /* + * Implement default commands + */ + + private void doAliasCommands(final String[] args) { + if (args.length != 2) { + errorOutput.accept("ERROR: Alias requires two arguments." + + " The command name, and the alias for that command"); + } else { + final String commandName = args[0]; + final String aliasName = args[1]; + + if (!canHandle(commandName)) { + errorOutput.accept("ERROR: '" + commandName + "' is not a valid command."); + } else if (canHandle(aliasName)) { + errorOutput.accept("ERROR: Cannot overwrite command '" + aliasName + "'"); + } else { + addCommandAlias(commandName, aliasName); + } + } + } + + private void doHelpCommand(final String commandName) { + if (commandHandlers.containsKey(commandName)) { + final String desc = commandHandlers.get(commandName).getHelp().getDescription(); + + normalOutput.accept("\n" + desc); + } else if (defaultHandlers.containsKey(commandName)) { + final String desc = defaultHandlers.get(commandName).getHelp().getDescription(); + + normalOutput.accept("\n" + desc); + } else if (helpTopics.containsKey(commandName)) { + normalOutput.accept("\n" + helpTopics.get(commandName).getDescription()); + } else { + errorOutput.accept( + "ERROR: I'm sorry, but there is no help available for '" + commandName + "'"); + } + } + + private void doHelpSummary() { + normalOutput.accept("Help topics for this command mode are as follows:\n"); + + if (commandHandlers.size() > 0) { + commandHandlers.forEachValue(command -> { + if (!command.isAlias()) { + normalOutput.accept("\t" + command.getHelp().getSummary() + "\n"); + } + }); + } else { + normalOutput.accept("\tNone available\n"); + } + + normalOutput.accept("\nHelp topics available in all command modes are as follows\n"); + if (defaultHandlers.size() > 0) { + defaultHandlers.forEachValue(command -> { + if (!command.isAlias()) { + normalOutput.accept("\t" + command.getHelp().getSummary() + "\n"); + } + }); + } else { + normalOutput.accept("\tNone available\n"); + } + + normalOutput.accept("\nHelp topics not associated with a command are as follows\n"); + if (helpTopics.size() > 0) { + helpTopics.forEachValue(topic -> { + normalOutput.accept("\t" + topic.getSummary() + "\n"); + }); + } else { + normalOutput.accept("\tNone available\n"); + } + } + + private void doListCommands() { + normalOutput.accept("The available commands for this mode are as follows:\n"); + + commandHandlers.keyList().forEach(commandName -> { + normalOutput.accept("\t" + commandName); + }); + + normalOutput.accept("\nThe following commands are available in all modes:\n"); + defaultHandlers.keyList().forEach(commandName -> { + normalOutput.accept("\t" + commandName); + }); + + normalOutput.accept("\n"); + } + + @Override + public String getCustomPrompt() { + if (customPrompt != null) return customPrompt; + + return CommandMode.super.getCustomPrompt(); + } + + @Override + public String getName() { + if (modeName != null) return modeName; + + return CommandMode.super.getName(); + } + + @Override + public boolean isCustomPromptEnabled() { + return customPrompt != null; + } + + @Override + public CommandMode process(final String command, final String[] args) { + normalOutput.accept("\n"); + + if (defaultHandlers.containsKey(command)) + return defaultHandlers.get(command).getHandler().handle(args); + else if (commandHandlers.containsKey(command)) + return commandHandlers.get(command).getHandler().handle(args); + else { + if (args != null) { + errorOutput.accept("ERROR: Unrecognized command " + command + String.join(" ", args)); + } else { + errorOutput.accept("ERROR: Unrecognized command " + command); + } + + if (unknownCommandHandler == null) + throw new UnsupportedOperationException("Command " + command + " is invalid."); + + unknownCommandHandler.accept(command, args); + } + + return this; + } + + /** + * Set the custom prompt for this mode + * + * @param prompt + * The custom prompt for this mode, or null to disable + * the custom prompt + */ + public void setCustomPrompt(final String prompt) { + customPrompt = prompt; + } + + /** + * Set the name of this mode + * + * @param name + * The desired name of this mode, or null to use the + * default name + */ + public void setModeName(final String name) { + modeName = name; + } + + /** + * Set the handler to use for unknown commands + * + * @param handler + * The handler to use for unknown commands, or null to + * throw on unknown commands + */ + public void setUnknownCommandHandler(final BiConsumer<String, String[]> handler) { + if (handler == null) throw new NullPointerException("Handler must not be null"); + + unknownCommandHandler = handler; + } + + private void setupDefaultCommands() { + defaultHandlers.put("list", buildListCommand()); + defaultHandlers.put("alias", buildAliasCommand()); + defaultHandlers.put("help", buildHelpCommand()); + + addCommandAlias("help", "man"); + + /* + * Add commands handled in a upper layer. + */ + + /* + * @TODO figure out a place to put commands that apply across + */ + /* + * all + */ + /* + * modes, but only apply to a specific application + */ + defaultHandlers.put("clear", buildClearCommands()); + defaultHandlers.put("exit", buildExitCommand()); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("GenericCommandMode ["); + + if (commandHandlers != null) { + builder.append("commandHandlers="); + builder.append(commandHandlers); + } + + if (defaultHandlers != null) { + builder.append(", "); + builder.append("defaultHandlers="); + builder.append(defaultHandlers); + } + + if (helpTopics != null) { + builder.append(", "); + builder.append("helpTopics="); + builder.append(helpTopics); + } + + if (modeName != null) { + builder.append(", "); + builder.append("modeName="); + builder.append(modeName); + } + + if (customPrompt != null) { + builder.append(", "); + builder.append("customPrompt="); + builder.append(customPrompt); + } + + builder.append("]"); + + return builder.toString(); + } + +} diff --git a/base/src/main/java/bjc/utils/cli/GenericHelp.java b/base/src/main/java/bjc/utils/cli/GenericHelp.java new file mode 100644 index 0000000..38adf57 --- /dev/null +++ b/base/src/main/java/bjc/utils/cli/GenericHelp.java @@ -0,0 +1,63 @@ +package bjc.utils.cli; + +/** + * Generic implementation of a help topic + * + * @author ben + * + */ +public class GenericHelp implements CommandHelp { + // The strings for this help topic + private final String summary; + private final String description; + + /** + * Create a new help topic + * + * @param summary + * The summary of this help topic + * @param description + * The description of this help topic, or null if this + * help topic doesn't have a more detailed description + */ + public GenericHelp(final String summary, final String description) { + if (summary == null) throw new NullPointerException("Help summary must be non-null"); + + this.summary = summary; + this.description = description; + } + + @Override + public String getDescription() { + if (description == null) return summary; + + return description; + } + + @Override + public String getSummary() { + return summary; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + + builder.append("GenericHelp ["); + + if (summary != null) { + builder.append("summary="); + builder.append(summary); + } + + if (description != null) { + builder.append(", "); + builder.append("description="); + builder.append(description); + } + + builder.append("]"); + + return builder.toString(); + } +} diff --git a/base/src/main/java/bjc/utils/cli/NullHelp.java b/base/src/main/java/bjc/utils/cli/NullHelp.java new file mode 100644 index 0000000..6c49ae6 --- /dev/null +++ b/base/src/main/java/bjc/utils/cli/NullHelp.java @@ -0,0 +1,20 @@ +package bjc.utils.cli; + +/** + * Implementation of a help topic that doesn't exist + * + * @author ben + * + */ +public class NullHelp implements CommandHelp { + @Override + public String getDescription() { + return "No description provided"; + } + + @Override + public String getSummary() { + return "No summary provided"; + } + +} diff --git a/base/src/main/java/bjc/utils/cli/objects/BlockReaderCLI.java b/base/src/main/java/bjc/utils/cli/objects/BlockReaderCLI.java new file mode 100644 index 0000000..ec66fe2 --- /dev/null +++ b/base/src/main/java/bjc/utils/cli/objects/BlockReaderCLI.java @@ -0,0 +1,392 @@ +package bjc.utils.cli.objects; + +import java.io.InputStreamReader; +import java.io.Reader; + +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; +import java.util.function.Predicate; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import bjc.utils.ioutils.Prompter; +import bjc.utils.ioutils.blocks.*; + +import static bjc.utils.cli.objects.Command.CommandStatus; +import static bjc.utils.cli.objects.Command.CommandStatus.*; + +public class BlockReaderCLI { + private final Logger LOGGER = Logger.getLogger(BlockReaderCLI.class.getName()); + + public static class BlockReaderState { + public final Map<String, BlockReader> readers; + public final Map<String, Reader> sources; + + public BlockReaderState(Map<String, BlockReader> readers, Map<String, Reader> sources) { + this.readers = readers; + this.sources = sources; + } + } + + private BlockReaderState stat; + + /** + * Create a new CLI for configuring BlockReaders. + * + * @param srcs + * The container of initial I/O sources. + */ + public BlockReaderCLI(Map<String, Reader> srcs) { + stat = new BlockReaderState(new HashMap<>(), srcs); + } + + public static void main(String[] args) { + /* + * Create/configure I/O sources. + */ + Map<String, Reader> sources = new HashMap<>(); + sources.put("stdio", new InputStreamReader(System.in)); + + BlockReaderCLI reader = new BlockReaderCLI(sources); + + reader.run(new Scanner(System.in), "console", true); + } + + /** + * Run the CLI on an input source. + * + * @param input + * The place to read input from. + * @param ioSource + * The name of the place to read input from. + * @param interactive + * Whether or not the source is interactive + */ + public void run(Scanner input, String ioSource, boolean interactive) { + int lno = 0; + while(input.hasNextLine()) { + if(interactive) + System.out.printf("reader-conf(%d)>", lno); + + String ln = input.nextLine(); + + lno += 1; + + Command com = Command.fromString(ln, lno, ioSource); + if(com == null) continue; + + CommandStatus stat = handleCommand(com, interactive); + if(stat == FINISH || stat == ERROR) { + return; + } + } + + input.close(); + } + + /* + * Handle a command. + */ + public CommandStatus handleCommand(Command com, boolean interactive) { + switch(com.nameCommand) { + case "def-filtered": + return defFiltered(com); + case "def-layered": + return defLayered(com); + case "def-pushback": + return defPushback(com); + case "def-simple": + return defSimple(com); + case "def-serial": + return defSerial(com); + case "def-toggled": + return defToggled(com); + case "}": + case "end": + case "exit": + case "quit": + if(interactive) + System.out.printf("Exiting reader-conf, %d readers configured in %d commands\n", + stat.readers.size(), com.lineNo); + return FINISH; + default: + LOGGER.severe(com.error("Unknown command '%s'\n", com.nameCommand)); + return FAIL; + } + } + + private CommandStatus defFiltered(Command com) { + String remn = com.remnCommand; + + /* + * Get the block name. + */ + int idx = remn.indexOf(' '); + if(idx == -1) { + LOGGER.severe(com.error("No name argument for def-filtered.\n")); + return FAIL; + } + String blockName = remn.substring(0, idx).trim(); + remn = remn.substring(idx).trim(); + + /* + * Check there isn't a reader already bound to this name. + */ + if(stat.readers.containsKey(blockName)) { + LOGGER.warning(com.warn("Shadowing existing reader named %s\n", blockName)); + } + + /* + * Get the reader name. + */ + idx = remn.indexOf(' '); + if(idx == -1) { + LOGGER.severe(com.error("No reader-name argument for def-filtered.\n")); + return FAIL; + } + String readerName = remn.substring(0, idx).trim(); + remn = remn.substring(idx).trim(); + + /* + * Check there is a reader bound to that name. + */ + if(!stat.readers.containsKey(readerName)) { + LOGGER.severe(com.error("No source named %s\n", readerName)); + return FAIL; + } + + /* + * Get the pattern. + */ + if(remn.equals("")) { + LOGGER.severe(com.error("No filter argument for def-filtered\n")); + return FAIL; + } + + String filter = remn; + + try { + Pattern pat = Pattern.compile(filter); + + Predicate<Block> pred = (block) -> { + Matcher mat = pat.matcher(block.contents); + + return mat.matches(); + }; + + BlockReader reader = new FilteredBlockReader(stat.readers.get(readerName), pred); + + stat.readers.put(blockName, reader); + } catch (PatternSyntaxException psex) { + LOGGER.severe(com.error("Invalid regular expression '%s' for filter. (%s)\n", filter, psex.getMessage())); + return FAIL; + } + + return SUCCESS; + } + + private CommandStatus defPushback(Command com) { + String[] parts = com.remnCommand.split(" "); + + if(parts.length != 2) { + LOGGER.severe(com.error("Incorrect number of arguments to def-pushback. Requires a block name and a reader name\n")); + return FAIL; + } + + String blockName = parts[0]; + if(stat.readers.containsKey(blockName)) { + LOGGER.warning(com.warn("Shadowing existing reader %s\n", blockName)); + return FAIL; + } + + String readerName = parts[1]; + if(!stat.readers.containsKey(readerName)) { + LOGGER.severe(com.error("No reader named %s\n", readerName)); + return FAIL; + } + + BlockReader reader = new PushbackBlockReader(stat.readers.get(readerName)); + stat.readers.put(blockName, reader); + + return SUCCESS; + } + + private CommandStatus defToggled(Command com) { + String[] parts = com.remnCommand.split(" "); + + if(parts.length != 3) { + LOGGER.severe(com.error("Incorrect number of arguments to def-toggled. Requires a block name and two reader names\n")); + return FAIL; + } + + /* + * Get the block name. + */ + String blockName = parts[0]; + if(stat.readers.containsKey(blockName)) { + LOGGER.warning(com.warn("Shadowing existing reader named %s\n", blockName)); + } + + /* + * Make sure the component readers exist. + */ + if(!stat.readers.containsKey(parts[1])) { + LOGGER.severe(com.error("No reader named %s\n", parts[1])); + return FAIL; + } + + if(!stat.readers.containsKey(parts[2])) { + LOGGER.severe(com.error("No reader named %s\n", parts[2])); + return FAIL; + } + + BlockReader reader = new ToggledBlockReader(stat.readers.get(parts[1]), stat.readers.get(parts[2])); + stat.readers.put(blockName, reader); + + return SUCCESS; + } + + private CommandStatus defLayered(Command com) { + String[] parts = com.remnCommand.split(" "); + + if(parts.length != 3) { + LOGGER.severe(com.error("Incorrect number of arguments to def-layered. Requires a block name and two reader names\n")); + return FAIL; + } + + /* + * Get the block name. + */ + String blockName = parts[0]; + if(stat.readers.containsKey(blockName)) { + LOGGER.warning(com.warn("Shadowing existing reader named %s\n", blockName)); + } + + /* + * Make sure the component readers exist. + */ + if(!stat.readers.containsKey(parts[1])) { + LOGGER.severe(com.error("No reader named %s\n", parts[1])); + return FAIL; + } + + if(!stat.readers.containsKey(parts[2])) { + LOGGER.severe(com.error("No reader named %s\n", parts[2])); + return FAIL; + } + + BlockReader reader = new LayeredBlockReader(stat.readers.get(parts[1]), stat.readers.get(parts[2])); + stat.readers.put(blockName, reader); + + return SUCCESS; + } + + private CommandStatus defSerial(Command com) { + String[] parts = com.remnCommand.split(" "); + + if(parts.length < 2) { + LOGGER.severe(com.error("Not enough arguments to def-serial. Requires at least a block name and at least one reader name\n")); + return FAIL; + } + + /* + * Get the name for this BlockReader. + */ + String blockName = parts[0]; + /* + * Check there isn't a reader already bound to this name. + */ + if(stat.readers.containsKey(blockName)) { + LOGGER.warning(com.warn("Shadowing existing reader named %s\n", blockName)); + } + + /* + * Get all of the component readers. + */ + BlockReader[] readerArr = new BlockReader[parts.length - 1]; + for(int i = 1; i < parts.length; i++) { + String readerName = parts[i]; + + /* + * Check there is a reader bound to that name. + */ + if(!stat.readers.containsKey(readerName)) { + LOGGER.severe(com.error("No reader named %s\n", readerName)); + return FAIL; + } + + readerArr[i] = stat.readers.get(readerName); + } + + BlockReader reader = new SerialBlockReader(readerArr); + + stat.readers.put(blockName, reader); + + return SUCCESS; + } + + private CommandStatus defSimple(Command com) { + String remn = com.remnCommand; + + /* + * Get the block name. + */ + int idx = remn.indexOf(' '); + if(idx == -1) { + LOGGER.severe(com.error("No name argument for def-simple.\n")); + return FAIL; + } + String blockName = remn.substring(0, idx).trim(); + remn = remn.substring(idx).trim(); + + /* + * Check there isn't a reader already bound to this name. + */ + if(stat.readers.containsKey(blockName)) { + LOGGER.warning(com.warn("Shadowing existing reader named %s\n", blockName)); + } + + /* + * Get the source name. + */ + idx = remn.indexOf(' '); + if(idx == -1) { + LOGGER.severe(com.error("No source-name argument for def-simple.\n")); + return FAIL; + } + String sourceName = remn.substring(0, idx).trim(); + remn = remn.substring(idx).trim(); + + /* + * Check there is a source bound to that name. + */ + if(!stat.sources.containsKey(sourceName)) { + LOGGER.severe(com.error("No source named %s\n", sourceName)); + return FAIL; + } + + /* + * Get the pattern. + */ + if(remn.equals("")) { + LOGGER.severe(com.error("No delimiter argument for def-simple\n")); + return FAIL; + } + + String delim = remn; + + try { + BlockReader reader = new SimpleBlockReader(delim, stat.sources.get(sourceName)); + + stat.readers.put(blockName, reader); + } catch (PatternSyntaxException psex) { + LOGGER.severe(com.error("Invalid regular expression '%s' for delimiter. (%s)\n", delim, psex.getMessage())); + return FAIL; + } + + return SUCCESS; + } +} diff --git a/base/src/main/java/bjc/utils/cli/objects/Command.java b/base/src/main/java/bjc/utils/cli/objects/Command.java new file mode 100644 index 0000000..e605a2b --- /dev/null +++ b/base/src/main/java/bjc/utils/cli/objects/Command.java @@ -0,0 +1,87 @@ +package bjc.utils.cli.objects; + +public class Command { + /** + * Command status values. + */ + public static enum CommandStatus { + /** + * The command succeded. + */ + SUCCESS, + /** + * The command failed non-fatally. + */ + FAIL, + /** + * The command failed fatally. + */ + ERROR, + /** + * The command was the last one. + */ + FINISH, + } + + public final int lineNo; + + public final String fullCommand; + public final String remnCommand; + public final String nameCommand; + + public final String ioSource; + + /** + * Create a new command. + * + * @param ln + * The line to get the command from. + * @param lno + * The number of the line the command came from. + * @param ioSrc + * The name of where the I/O came from. + */ + public Command(String ln, int lno, String ioSrc) { + int idx = ln.indexOf(' '); + + if(idx == -1) idx = ln.length(); + + fullCommand = ln; + nameCommand = ln.substring(0, idx).trim(); + remnCommand = ln.substring(idx).trim(); + + lineNo = lno; + + ioSource = ioSrc; + } + + public static Command fromString(String ln, int lno, String ioSource) { + /* + * Ignore blank lines and comments. + */ + if(ln.equals("")) return null; + if(ln.startsWith("#")) return null; + + /* + * Trim off comments part-way through the line. + */ + int idxHash = ln.indexOf('#'); + if(idxHash != -1) { + ln = ln.substring(0, idxHash).trim(); + } + + return new Command(ln, lno, ioSource); + } + + public String warn(String warning, Object... parms) { + String msg = String.format(warning, parms); + + return String.format("WARNING (%s:%d): %s", ioSource, lineNo, msg); + } + + public String error(String err, Object... parms) { + String msg = String.format(err, parms); + + return String.format("ERROR (%s:%d): %s", ioSource, lineNo, msg); + } +} diff --git a/base/src/main/java/bjc/utils/cli/objects/DefineCLI.java b/base/src/main/java/bjc/utils/cli/objects/DefineCLI.java new file mode 100644 index 0000000..bb2733f --- /dev/null +++ b/base/src/main/java/bjc/utils/cli/objects/DefineCLI.java @@ -0,0 +1,133 @@ +package bjc.utils.cli.objects; + +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; +import java.util.function.UnaryOperator; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +import static bjc.utils.cli.objects.Command.CommandStatus; +import static bjc.utils.cli.objects.Command.CommandStatus.*; + +public class DefineCLI { + private final Logger LOGGER = Logger.getLogger(DefineCLI.class.getName()); + + public static class DefineState { + public final Map<String, UnaryOperator<String>> defines; + + public final Map<String, String> strings; + public final Map<String, String> formats; + + public final Map<String, Pattern> patterns; + + public DefineState() { + this(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>()); + } + + public DefineState(Map<String, UnaryOperator<String>> defines, + Map<String, String> strings, Map<String, String> formats, + Map<String, Pattern> patterns) { + this.defines = defines; + + this.strings = strings; + this.formats = formats; + + this.patterns = patterns; + } + } + + private DefineState stat; + + public DefineCLI() { + stat = new DefineState(); + } + + public static void main(String[] args) { + DefineCLI defin = new DefineCLI(); + } + + /** + * Run the CLI on an input source. + * + * @param input + * The place to read input from. + * @param ioSource + * The name of the place to read input from. + * @param interactive + * Whether or not the source is interactive + */ + public void run(Scanner input, String ioSource, boolean interactive) { + int lno = 0; + while(input.hasNextLine()) { + if(interactive) + System.out.printf("define-conf(%d)>", lno); + + String ln = input.nextLine(); + + lno += 1; + + Command com = Command.fromString(ln, lno, ioSource); + if(com == null) continue; + + handleCommand(com, interactive); + } + + input.close(); + } + + public void handleCommand(Command com, boolean interactive) { + switch(com.nameCommand) { + case "def-string": + default: + LOGGER.severe(com.error("Unknown command %s\n", com.nameCommand)); + break; + } + } + + private CommandStatus defString(Command com) { + String remn = com.remnCommand; + + int idx = remn.indexOf(' '); + if(idx == -1) { + LOGGER.warning(com.warn("Binding empty string to name '%s'\n", remn)); + idx = remn.length(); + } + String name = remn.substring(0, idx); + String strang = remn.substring(idx); + + if(stat.strings.containsKey(name)) { + LOGGER.warning(com.warn("Shadowing string '%s'\n", name)); + } + + stat.strings.put(name, strang); + + return SUCCESS; + } + + private CommandStatus defFormat(Command com) { + String remn = com.remnCommand; + + int idx = remn.indexOf(' '); + if(idx == -1) { + LOGGER.warning(com.warn("Binding empty format to name '%s'\n", remn)); + idx = remn.length(); + } + String name = remn.substring(0, idx); + String fmt = remn.substring(idx); + + if(stat.formats.containsKey(name)) { + LOGGER.warning(com.warn("Shadowing format '%s'\n", name)); + } + + stat.formats.put(name, fmt); + + return SUCCESS; + } + + private CommandStatus bindFormat(Command com) { + String[] parts = com.remnCommand.split(" "); + + return SUCCESS; + } +} diff --git a/base/src/main/java/bjc/utils/components/ComponentDescription.java b/base/src/main/java/bjc/utils/components/ComponentDescription.java new file mode 100644 index 0000000..28f81d1 --- /dev/null +++ b/base/src/main/java/bjc/utils/components/ComponentDescription.java @@ -0,0 +1,135 @@ +package bjc.utils.components; + +/** + * Generic implementation of a description for a component + * + * @author ben + * + */ +public class ComponentDescription implements IDescribedComponent { + private static void sanityCheckArgs(final String name, final String author, final String description, + final int version) { + if (name == null) + throw new NullPointerException("Component name can't be null"); + else if (version <= 0) throw new IllegalArgumentException("Component version must be greater than 0"); + } + + /** + * The author of the component + */ + private final String author; + /** + * The description of the component + */ + private final String description; + /** + * The name of the component + */ + private final String name; + + /** + * The version of the component + */ + private final int version; + + /** + * Create a new component description + * + * @param name + * The name of the component + * @param author + * The author of the component + * @param description + * The description of the component + * @param version + * The version of the component + * @throws IllegalArgumentException + * thrown if version is less than 1 + */ + public ComponentDescription(final String name, final String author, final String description, + final int version) { + sanityCheckArgs(name, author, description, version); + + this.name = name; + this.author = author; + this.description = description; + this.version = version; + } + + @Override + public String getAuthor() { + if (author == null) return IDescribedComponent.super.getAuthor(); + + return author; + } + + @Override + public String getDescription() { + if (description == null) return IDescribedComponent.super.getDescription(); + + return description; + } + + @Override + public String getName() { + return name; + } + + @Override + public int getVersion() { + return version; + } + + @Override + public String toString() { + return name + " component v" + version + ", written by " + author; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + + result = prime * result + (author == null ? 0 : author.hashCode()); + result = prime * result + (description == null ? 0 : description.hashCode()); + result = prime * result + (name == null ? 0 : name.hashCode()); + result = prime * result + version; + + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + + final ComponentDescription other = (ComponentDescription) obj; + + if (author == null) { + if (other.author != null) return false; + } else if (!author.equals(other.author)) return false; + + if (description == null) { + if (other.description != null) return false; + } else if (!description.equals(other.description)) return false; + + if (name == null) { + if (other.name != null) return false; + } else if (!name.equals(other.name)) return false; + + if (version != other.version) return false; + + return true; + } +} diff --git a/base/src/main/java/bjc/utils/components/ComponentDescriptionFileParser.java b/base/src/main/java/bjc/utils/components/ComponentDescriptionFileParser.java new file mode 100644 index 0000000..f7ddaff --- /dev/null +++ b/base/src/main/java/bjc/utils/components/ComponentDescriptionFileParser.java @@ -0,0 +1,65 @@ +package bjc.utils.components; + +import static bjc.utils.ioutils.RuleBasedReaderPragmas.buildInteger; +import static bjc.utils.ioutils.RuleBasedReaderPragmas.buildStringCollapser; + +import java.io.InputStream; + +import bjc.utils.ioutils.RuleBasedConfigReader; + +/** + * Read a component description from a file + * + * @author ben + * + */ +public class ComponentDescriptionFileParser { + // The reader used to read in component descriptions + private static RuleBasedConfigReader<ComponentDescriptionState> reader; + + // Initialize the reader and its pragmas + static { + // This reader works entirely off of pragmas, so no need to + // handle + // rules + reader = new RuleBasedConfigReader<>((tokenizer, statePair) -> { + // Don't need to do anything on rule start + }, (tokenizer, state) -> { + // Don't need to do anything on rule continuation + }, (state) -> { + // Don't need to do anything on rule end + }); + + setupReaderPragmas(); + } + + /** + * Parse a component description from a stream + * + * @param inputSource + * The stream to parse from + * @return The description parsed from the stream + */ + public static ComponentDescription fromStream(final InputStream inputSource) { + if (inputSource == null) throw new NullPointerException("Input source must not be null"); + + final ComponentDescriptionState readState = reader.fromStream(inputSource, + new ComponentDescriptionState()); + + return readState.toDescription(); + } + + /* + * Create all the pragmas the reader needs to function + */ + private static void setupReaderPragmas() { + reader.addPragma("name", buildStringCollapser("name", (name, state) -> state.setName(name))); + + reader.addPragma("author", buildStringCollapser("author", (author, state) -> state.setAuthor(author))); + + reader.addPragma("description", buildStringCollapser("description", + (description, state) -> state.setDescription(description))); + + reader.addPragma("version", buildInteger("version", (version, state) -> state.setVersion(version))); + } +} diff --git a/base/src/main/java/bjc/utils/components/ComponentDescriptionState.java b/base/src/main/java/bjc/utils/components/ComponentDescriptionState.java new file mode 100644 index 0000000..8d66f85 --- /dev/null +++ b/base/src/main/java/bjc/utils/components/ComponentDescriptionState.java @@ -0,0 +1,144 @@ +package bjc.utils.components; + +/** + * Internal state of component description parser + * + * @author ben + * + */ +public class ComponentDescriptionState { + // Tentative name of this component + private String name; + + // Tentative description of this componet + private String description; + + // Tentative author of this component + private String author; + + // Tentative version of this component + private int version; + + /** + * Set the author of this component + * + * @param author + * The author of this component + */ + public void setAuthor(final String author) { + this.author = author; + } + + /** + * Set the description of this component + * + * @param description + * The description of this component + */ + public void setDescription(final String description) { + this.description = description; + } + + /** + * Set the name of this component + * + * @param name + * The name of this component + */ + public void setName(final String name) { + this.name = name; + } + + /** + * Set the version of this component + * + * @param version + * The version of this component + */ + public void setVersion(final int version) { + this.version = version; + } + + /** + * Convert this state into the description it represents + * + * @return The description represented by this state + */ + public ComponentDescription toDescription() { + return new ComponentDescription(name, author, description, version); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + + result = prime * result + (author == null ? 0 : author.hashCode()); + result = prime * result + (description == null ? 0 : description.hashCode()); + result = prime * result + (name == null ? 0 : name.hashCode()); + result = prime * result + version; + + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + + final ComponentDescriptionState other = (ComponentDescriptionState) obj; + + if (author == null) { + if (other.author != null) return false; + } else if (!author.equals(other.author)) return false; + + if (description == null) { + if (other.description != null) return false; + } else if (!description.equals(other.description)) return false; + + if (name == null) { + if (other.name != null) return false; + } else if (!name.equals(other.name)) return false; + + if (version != other.version) return false; + + return true; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("ComponentDescriptionState ["); + + if (name != null) { + builder.append("name="); + builder.append(name); + builder.append(", "); + } + + if (description != null) { + builder.append("description="); + builder.append(description); + builder.append(", "); + } + + if (author != null) { + builder.append("author="); + builder.append(author); + builder.append(", "); + } + + builder.append("version="); + builder.append(version); + builder.append("]"); + + return builder.toString(); + } + +} diff --git a/base/src/main/java/bjc/utils/components/FileComponentRepository.java b/base/src/main/java/bjc/utils/components/FileComponentRepository.java new file mode 100644 index 0000000..efde5c7 --- /dev/null +++ b/base/src/main/java/bjc/utils/components/FileComponentRepository.java @@ -0,0 +1,181 @@ +package bjc.utils.components; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; + +import bjc.utils.data.IHolder; +import bjc.utils.data.Identity; +import bjc.utils.funcdata.FunctionalMap; +import bjc.utils.funcdata.IList; +import bjc.utils.funcdata.IMap; +import bjc.utils.funcutils.FileUtils; + +/** + * A component repository that loads its components from files in a directory + * + * @author ben + * + * @param <ComponentType> + * The type of component being read in + */ +public class FileComponentRepository<ComponentType extends IDescribedComponent> + implements IComponentRepository<ComponentType> { + // The logger to use for storing data about this class + private static final Logger CLASS_LOGGER = Logger.getLogger("FileComponentRepository"); + + // The internal storage of components + private IMap<String, ComponentType> components; + + // The path that all the components came from + private Path sourceDirectory; + + /** + * Create a new component repository sourcing components from files in a + * directory + * + * An exception thrown during the loading of a component will only cause + * the loading of that component to fail, but a warning will be logged. + * + * @param directory + * The directory to read component files from + * @param componentReader + * The function to use to convert files to components + */ + public FileComponentRepository(final File directory, + final Function<File, ? extends ComponentType> componentReader) { + // Make sure we have valid arguments + if (directory == null) + throw new NullPointerException("Directory must not be null"); + else if (!directory.isDirectory()) + throw new IllegalArgumentException("File " + directory + " is not a directory.\n" + + "Components can only be read from a directory"); + else if (componentReader == null) throw new NullPointerException("Component reader must not be null"); + + // Initialize our fields + components = new FunctionalMap<>(); + sourceDirectory = directory.toPath().toAbsolutePath(); + + // Marker for making sure we don't skip the parent + final IHolder<Boolean> isFirstDir = new Identity<>(true); + + // Predicate to use to traverse all the files in a directory, + // but + // not recurse into sub-directories + final BiPredicate<Path, BasicFileAttributes> firstLevelTraverser = (pth, attr) -> { + if (attr.isDirectory() && !isFirstDir.getValue()) /* + * Skip + * directories, + * they + * probably + * have + * component + * support + * files. + */ + return false; + + /* + * Don't skip the first directory, that's the parent + * directory + */ + isFirstDir.replace(false); + + return true; + }; + + // Try reading components + try { + FileUtils.traverseDirectory(sourceDirectory, firstLevelTraverser, (pth, attr) -> { + loadComponent(componentReader, pth); + + // Keep loading components, even if this one + // failed + return true; + }); + } catch (final IOException ioex) { + CLASS_LOGGER.log(Level.WARNING, ioex, () -> "Error found reading component from file."); + } + } + + @Override + public IMap<String, ComponentType> getAll() { + return components; + } + + @Override + public ComponentType getByName(final String name) { + return components.get(name); + } + + @Override + public IList<ComponentType> getList() { + return components.valueList(); + } + + @Override + public String getSource() { + return "Components read from directory " + sourceDirectory + "."; + } + + /* + * Load a component from a file + */ + private void loadComponent(final Function<File, ? extends ComponentType> componentReader, final Path pth) { + try { + // Try to load the component + final ComponentType component = componentReader.apply(pth.toFile()); + + if (component == null) + throw new NullPointerException("Component reader read null component"); + else if (!components.containsKey(component.getName())) { + // We only care about the latest version of a + // component + final ComponentType oldComponent = components.put(component.getName(), component); + + if (oldComponent.getVersion() > component.getVersion()) { + components.put(oldComponent.getName(), oldComponent); + } + } else { + CLASS_LOGGER.warning("Found a duplicate component.\n" + + "Multiple versions of the same component are not currently supported.\n" + + "Only the latest version of the component" + component + + " will be registered ."); + } + } catch (final Exception ex) { + CLASS_LOGGER.log(Level.WARNING, ex, () -> "Error found reading component from file " + + pth.toString() + ". This component will not be loaded"); + } + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("FileComponentRepository ["); + + if (components != null) { + builder.append("components="); + builder.append(components); + builder.append(", "); + } + + if (sourceDirectory != null) { + builder.append("sourceDirectory="); + builder.append(sourceDirectory); + } + + builder.append("]"); + + return builder.toString(); + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/components/IComponentRepository.java b/base/src/main/java/bjc/utils/components/IComponentRepository.java new file mode 100644 index 0000000..6ee51f3 --- /dev/null +++ b/base/src/main/java/bjc/utils/components/IComponentRepository.java @@ -0,0 +1,49 @@ +package bjc.utils.components; + +import bjc.utils.funcdata.IList; +import bjc.utils.funcdata.IMap; + +/** + * A collection of implementations of a particular type of + * {@link IDescribedComponent} + * + * @author ben + * + * @param <ComponentType> + * The type of components contained in this repository + */ +public interface IComponentRepository<ComponentType extends IDescribedComponent> { + /** + * Get all of the components this repository knows about + * + * @return A map from component name to component, containing all of the + * components in the repositories + */ + public IMap<String, ComponentType> getAll(); + + /** + * Get a component with a specific name + * + * @param name + * The name of the component to retrieve + * @return The named component, or null if no component with that name + * exists + */ + public ComponentType getByName(String name); + + /** + * Get a list of all the registered components + * + * @return A list of all the registered components + */ + public default IList<ComponentType> getList() { + return getAll().valueList(); + } + + /** + * Get the source from which these components came + * + * @return The source from which these components came + */ + public String getSource(); +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/components/IDescribedComponent.java b/base/src/main/java/bjc/utils/components/IDescribedComponent.java new file mode 100644 index 0000000..952b375 --- /dev/null +++ b/base/src/main/java/bjc/utils/components/IDescribedComponent.java @@ -0,0 +1,64 @@ +package bjc.utils.components; + +/** + * Represents a optional component that has status information associated with + * it + * + * @author ben + * + */ +public interface IDescribedComponent extends Comparable<IDescribedComponent> { + /** + * Get the author of this component + * + * Providing this is optional, with "Anonymous" as the default author + * + * @return The author of the component + */ + default String getAuthor() { + return "Anonymous"; + } + + /** + * Get the description of this component + * + * Providing this is optional, with the default being a note that no + * description was provided + * + * @return The description of the component + */ + default String getDescription() { + return "No description provided."; + } + + /** + * Get the name of this component. + * + * This is the only thing required of all components + * + * @return The name of the component + */ + String getName(); + + /** + * Get the version of this component + * + * Providing this is optional, with "1" as the default version + * + * @return The version of this component + */ + default int getVersion() { + return 1; + } + + @Override + default int compareTo(final IDescribedComponent o) { + int res = getName().compareTo(o.getName()); + + if (res == 0) { + res = getVersion() - o.getVersion(); + } + + return res; + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/data/BooleanToggle.java b/base/src/main/java/bjc/utils/data/BooleanToggle.java new file mode 100644 index 0000000..12e3b2e --- /dev/null +++ b/base/src/main/java/bjc/utils/data/BooleanToggle.java @@ -0,0 +1,76 @@ +package bjc.utils.data; + +/** + * A simple {@link ValueToggle} that swaps between true and false. + * + * @author EVE + * + */ +public class BooleanToggle implements Toggle<Boolean> { + private boolean val; + + /** + * Create a new, initially false, flip-flop. + */ + public BooleanToggle() { + this(false); + } + + /** + * Create a flip-flop with the specified initial value. + * + * @param initial + * The initial value of the flip-flop. + */ + public BooleanToggle(final boolean initial) { + val = initial; + } + + @Override + public Boolean get() { + final boolean res = val; + + val = !res; + + return res; + } + + @Override + public Boolean peek() { + return val; + } + + @Override + public void set(final boolean vl) { + val = vl; + } + + @Override + public int hashCode() { + final int prime = 31; + + int result = 1; + + result = prime * result + (val ? 1231 : 1237); + + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof BooleanToggle)) return false; + + final BooleanToggle other = (BooleanToggle) obj; + + if (val != other.val) return false; + + return true; + } + + @Override + public String toString() { + return String.format("BooleanToggle [val=%s]", val); + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/data/CircularIterator.java b/base/src/main/java/bjc/utils/data/CircularIterator.java new file mode 100644 index 0000000..a708eba --- /dev/null +++ b/base/src/main/java/bjc/utils/data/CircularIterator.java @@ -0,0 +1,81 @@ +package bjc.utils.data; + +import java.util.Iterator; + +/** + * An iterator that repeats elements from a provided iterable. + * + * @author EVE + * + * @param <E> + * The type of the iterable. + */ +public class CircularIterator<E> implements Iterator<E> { + /* + * The iterable, and our current iterator into it. + */ + private Iterable<E> source; + private Iterator<E> curr; + + /* + * Our current element. + */ + private E curElm; + + /* + * Should we actually get new iterators, or just repeat the last + * element? + */ + private boolean doCircle; + + /** + * Create a new circular iterator. + * + * @param src + * The iterable to iterate from. + * + * @param circ + * Should we actually do circular iteration, or just + * repeat the terminal element? + */ + public CircularIterator(final Iterable<E> src, final boolean circ) { + source = src; + curr = source.iterator(); + + doCircle = circ; + } + + /** + * Create a new circular iterator that does actual circular iteration. + * + * @param src + * The iterable to iterate from. + */ + public CircularIterator(final Iterable<E> src) { + this(src, true); + } + + @Override + public boolean hasNext() { + // We always have something + return true; + } + + @Override + public E next() { + if (!curr.hasNext()) { + if (doCircle) { + curr = source.iterator(); + } else return curElm; + } + + curElm = curr.next(); + + return curElm; + } + + @Override + public void remove() { + curr.remove(); + } +} diff --git a/base/src/main/java/bjc/utils/data/Either.java b/base/src/main/java/bjc/utils/data/Either.java new file mode 100644 index 0000000..36b3324 --- /dev/null +++ b/base/src/main/java/bjc/utils/data/Either.java @@ -0,0 +1,173 @@ +package bjc.utils.data; + +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Represents a pair where only one side has a value + * + * @author ben + * @param <LeftType> + * The type that could be on the left + * @param <RightType> + * The type that could be on the right + * + */ +public class Either<LeftType, RightType> implements IPair<LeftType, RightType> { + /** + * Create a new either with the left value occupied + * + * @param <LeftType> + * The type of the left value + * @param <RightType> + * The type of the empty right value + * @param left + * The value to put on the left + * @return An either with the left side occupied + */ + public static <LeftType, RightType> Either<LeftType, RightType> left(final LeftType left) { + return new Either<>(left, null); + } + + /** + * Create a new either with the right value occupied + * + * @param <LeftType> + * The type of the empty left value + * @param <RightType> + * The type of the right value + * @param right + * The value to put on the right + * @return An either with the right side occupied + */ + public static <LeftType, RightType> Either<LeftType, RightType> right(final RightType right) { + return new Either<>(null, right); + } + + private LeftType leftVal; + + private RightType rightVal; + + private boolean isLeft; + + private Either(final LeftType left, final RightType right) { + if (left == null) { + rightVal = right; + } else { + leftVal = left; + + isLeft = true; + } + } + + @Override + public <BoundLeft, BoundRight> IPair<BoundLeft, BoundRight> bind( + final BiFunction<LeftType, RightType, IPair<BoundLeft, BoundRight>> binder) { + if (binder == null) throw new NullPointerException("Binder must not be null"); + + return binder.apply(leftVal, rightVal); + } + + @Override + public <BoundLeft> IPair<BoundLeft, RightType> bindLeft( + final Function<LeftType, IPair<BoundLeft, RightType>> leftBinder) { + if (leftBinder == null) throw new NullPointerException("Left binder must not be null"); + + if (isLeft) return leftBinder.apply(leftVal); + + return new Either<>(null, rightVal); + } + + @Override + public <BoundRight> IPair<LeftType, BoundRight> bindRight( + final Function<RightType, IPair<LeftType, BoundRight>> rightBinder) { + if (rightBinder == null) throw new NullPointerException("Right binder must not be null"); + + if (isLeft) return new Either<>(leftVal, null); + + return rightBinder.apply(rightVal); + } + + @Override + public <OtherLeft, OtherRight, CombinedLeft, CombinedRight> IPair<CombinedLeft, CombinedRight> combine( + final IPair<OtherLeft, OtherRight> otherPair, + final BiFunction<LeftType, OtherLeft, CombinedLeft> leftCombiner, + final BiFunction<RightType, OtherRight, CombinedRight> rightCombiner) { + if (otherPair == null) + throw new NullPointerException("Other pair must not be null"); + else if (leftCombiner == null) + throw new NullPointerException("Left combiner must not be null"); + else if (rightCombiner == null) throw new NullPointerException("Right combiner must not be null"); + + if (isLeft) return otherPair.bind((otherLeft, otherRight) -> { + return new Either<>(leftCombiner.apply(leftVal, otherLeft), null); + }); + + return otherPair.bind((otherLeft, otherRight) -> { + return new Either<>(null, rightCombiner.apply(rightVal, otherRight)); + }); + } + + @Override + public <NewLeft> IPair<NewLeft, RightType> mapLeft(final Function<LeftType, NewLeft> mapper) { + if (mapper == null) throw new NullPointerException("Mapper must not be null"); + + if (isLeft) return new Either<>(mapper.apply(leftVal), null); + + return new Either<>(null, rightVal); + } + + @Override + public <NewRight> IPair<LeftType, NewRight> mapRight(final Function<RightType, NewRight> mapper) { + if (mapper == null) throw new NullPointerException("Mapper must not be null"); + + if (isLeft) return new Either<>(leftVal, null); + + return new Either<>(null, mapper.apply(rightVal)); + } + + @Override + public <MergedType> MergedType merge(final BiFunction<LeftType, RightType, MergedType> merger) { + if (merger == null) throw new NullPointerException("Merger must not be null"); + + return merger.apply(leftVal, rightVal); + } + + @Override + public int hashCode() { + final int prime = 31; + + int result = 1; + result = prime * result + (isLeft ? 1231 : 1237); + result = prime * result + (leftVal == null ? 0 : leftVal.hashCode()); + result = prime * result + (rightVal == null ? 0 : rightVal.hashCode()); + + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof Either<?, ?>)) return false; + + final Either<?, ?> other = (Either<?, ?>) obj; + + if (isLeft != other.isLeft) return false; + + if (leftVal == null) { + if (other.leftVal != null) return false; + } else if (!leftVal.equals(other.leftVal)) return false; + + if (rightVal == null) { + if (other.rightVal != null) return false; + } else if (!rightVal.equals(other.rightVal)) return false; + + return true; + } + + @Override + public String toString() { + return String.format("Either [leftVal='%s', rightVal='%s', isLeft=%s]", leftVal, rightVal, isLeft); + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/data/GeneratingIterator.java b/base/src/main/java/bjc/utils/data/GeneratingIterator.java new file mode 100644 index 0000000..9abca7c --- /dev/null +++ b/base/src/main/java/bjc/utils/data/GeneratingIterator.java @@ -0,0 +1,53 @@ +package bjc.utils.data; + +import java.util.Iterator; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +/** + * An iterator that generates a series of elements from a single element. + * + * @author bjculkin + * + * @param <E> + * The type of element generated. + */ +public class GeneratingIterator<E> implements Iterator<E> { + private E state; + + private UnaryOperator<E> transtion; + + private Predicate<E> stpper; + + /** + * Create a new generative iterator. + * + * @param initial + * The initial state of the generator. + * + * @param transition + * The function to apply to the state. + * + * @param stopper + * The predicate applied to the current state to + * determine when to stop. + */ + public GeneratingIterator(E initial, UnaryOperator<E> transition, Predicate<E> stopper) { + state = initial; + transtion = transition; + stpper = stopper; + } + + @Override + public boolean hasNext() { + return stpper.test(state); + } + + @Override + public E next() { + state = transtion.apply(state); + + return state; + } + +} diff --git a/base/src/main/java/bjc/utils/data/IHolder.java b/base/src/main/java/bjc/utils/data/IHolder.java new file mode 100644 index 0000000..ca0b2ba --- /dev/null +++ b/base/src/main/java/bjc/utils/data/IHolder.java @@ -0,0 +1,153 @@ +package bjc.utils.data; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +import bjc.utils.data.internals.BoundListHolder; +import bjc.utils.data.internals.WrappedLazy; +import bjc.utils.data.internals.WrappedOption; +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.theory.Functor; + +/** + * A holder of a single value. + * + * @author ben + * + * @param <ContainedType> + * The type of value held + */ +public interface IHolder<ContainedType> extends Functor<ContainedType> { + /** + * Bind a function across the value in this container + * + * @param <BoundType> + * The type of value in this container + * @param binder + * The function to bind to the value + * @return A holder from binding the value + */ + public <BoundType> IHolder<BoundType> bind(Function<ContainedType, IHolder<BoundType>> binder); + + /** + * Apply an action to the value + * + * @param action + * The action to apply to the value + */ + public default void doWith(final Consumer<? super ContainedType> action) { + transform(value -> { + action.accept(value); + + return value; + }); + } + + @Override + default <ArgType, ReturnType> Function<Functor<ArgType>, Functor<ReturnType>> fmap( + final Function<ArgType, ReturnType> func) { + return argumentFunctor -> { + if (!(argumentFunctor instanceof IHolder<?>)) { + final String msg = "This functor only supports mapping over instances of IHolder"; + + throw new IllegalArgumentException(msg); + } + + final IHolder<ArgType> holder = (IHolder<ArgType>) argumentFunctor; + + return holder.map(func); + }; + } + + @Override + public default ContainedType getValue() { + return unwrap(value -> value); + } + + /** + * Lifts a function to bind over this holder + * + * @param <NewType> + * The type of the functions return + * @param func + * The function to lift over the holder + * @return The function lifted over the holder + */ + public <NewType> Function<ContainedType, IHolder<NewType>> lift(Function<ContainedType, NewType> func); + + /** + * Make this holder lazy + * + * @return A lazy version of this holder + */ + public default IHolder<ContainedType> makeLazy() { + return new WrappedLazy<>(this); + } + + /** + * Make this holder a list + * + * @return A list version of this holder + */ + public default IHolder<ContainedType> makeList() { + return new BoundListHolder<>(new FunctionalList<>(this)); + } + + /** + * Make this holder optional + * + * @return An optional version of this holder + */ + public default IHolder<ContainedType> makeOptional() { + return new WrappedOption<>(this); + } + + /** + * Create a new holder with a mapped version of the value in this + * holder. + * + * Does not change the internal state of this holder + * + * @param <MappedType> + * The type of the mapped value + * @param mapper + * The function to do mapping with + * @return A holder with the mapped value + */ + public <MappedType> IHolder<MappedType> map(Function<ContainedType, MappedType> mapper); + + /** + * Replace the held value with a new one + * + * @param newValue + * The value to hold instead + * @return The holder itself + */ + public default IHolder<ContainedType> replace(final ContainedType newValue) { + return transform(oldValue -> { + return newValue; + }); + } + + /** + * Transform the value held in this holder + * + * @param transformer + * The function to transform the value with + * @return The holder itself, for easy chaining + */ + public IHolder<ContainedType> transform(UnaryOperator<ContainedType> transformer); + + /** + * Unwrap the value contained in this holder so that it is no longer + * held + * + * @param <UnwrappedType> + * The type of the unwrapped value + * @param unwrapper + * The function to use to unwrap the value + * @return The unwrapped held value + */ + public <UnwrappedType> UnwrappedType unwrap(Function<ContainedType, UnwrappedType> unwrapper); +} diff --git a/base/src/main/java/bjc/utils/data/IPair.java b/base/src/main/java/bjc/utils/data/IPair.java new file mode 100644 index 0000000..db8a1cb --- /dev/null +++ b/base/src/main/java/bjc/utils/data/IPair.java @@ -0,0 +1,200 @@ +package bjc.utils.data; + +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; + +import bjc.utils.funcdata.theory.Bifunctor; + +/** + * Represents a pair of values + * + * @author ben + * @param <LeftType> + * The type of the left side of the pair + * @param <RightType> + * The type of the right side of the pair + * + */ +public interface IPair<LeftType, RightType> extends Bifunctor<LeftType, RightType> { + /** + * Bind a function across the values in this pair + * + * @param <BoundLeft> + * The type of the bound left + * @param <BoundRight> + * The type of the bound right + * @param binder + * The function to bind with + * @return The bound pair + */ + public <BoundLeft, BoundRight> IPair<BoundLeft, BoundRight> bind( + BiFunction<LeftType, RightType, IPair<BoundLeft, BoundRight>> binder); + + /** + * Bind a function to the left value in this pair + * + * @param <BoundLeft> + * The type of the bound value + * @param leftBinder + * The function to use to bind + * @return A pair with the left type bound + */ + public <BoundLeft> IPair<BoundLeft, RightType> bindLeft( + Function<LeftType, IPair<BoundLeft, RightType>> leftBinder); + + /** + * Bind a function to the right value in this pair + * + * @param <BoundRight> + * The type of the bound value + * @param rightBinder + * The function to use to bind + * @return A pair with the right type bound + */ + public <BoundRight> IPair<LeftType, BoundRight> bindRight( + Function<RightType, IPair<LeftType, BoundRight>> rightBinder); + + /** + * Pairwise combine two pairs together + * + * @param <OtherLeft> + * The left type of the other pair + * @param <OtherRight> + * The right type of the other pair + * @param otherPair + * The pair to combine with + * @return The pairs, pairwise combined together + */ + public default <OtherLeft, OtherRight> IPair<IPair<LeftType, OtherLeft>, IPair<RightType, OtherRight>> combine( + final IPair<OtherLeft, OtherRight> otherPair) { + return combine(otherPair, Pair<LeftType, OtherLeft>::new, Pair<RightType, OtherRight>::new); + } + + /** + * Combine the contents of two pairs together + * + * @param <OtherLeft> + * The type of the left value of the other pair + * @param <OtherRight> + * The type of the right value of the other pair + * @param <CombinedLeft> + * The type of the left value of the combined pair + * @param <CombinedRight> + * The type of the right value of the combined pair + * @param otherPair + * The other pair to combine with + * @param leftCombiner + * @param rightCombiner + * @return A pair with its values combined + */ + public <OtherLeft, OtherRight, CombinedLeft, CombinedRight> IPair<CombinedLeft, CombinedRight> combine( + IPair<OtherLeft, OtherRight> otherPair, + BiFunction<LeftType, OtherLeft, CombinedLeft> leftCombiner, + BiFunction<RightType, OtherRight, CombinedRight> rightCombiner); + + /** + * Immediately perfom the specified action with the contents of this + * pair + * + * @param consumer + * The action to perform on the pair + */ + public default void doWith(final BiConsumer<LeftType, RightType> consumer) { + merge((leftValue, rightValue) -> { + consumer.accept(leftValue, rightValue); + + return null; + }); + } + + @Override + default <OldLeft, OldRight, NewLeft> LeftBifunctorMap<OldLeft, OldRight, NewLeft> fmapLeft( + final Function<OldLeft, NewLeft> func) { + return argumentPair -> { + if (!(argumentPair instanceof IPair<?, ?>)) { + final String msg = "This function can only be applied to instances of IPair"; + + throw new IllegalArgumentException(msg); + } + + final IPair<OldLeft, OldRight> argPair = (IPair<OldLeft, OldRight>) argumentPair; + + return argPair.mapLeft(func); + }; + } + + @Override + default <OldLeft, OldRight, NewRight> RightBifunctorMap<OldLeft, OldRight, NewRight> + + fmapRight(final Function<OldRight, NewRight> func) { + return argumentPair -> { + if (!(argumentPair instanceof IPair<?, ?>)) { + final String msg = "This function can only be applied to instances of IPair"; + + throw new IllegalArgumentException(msg); + } + + final IPair<OldLeft, OldRight> argPair = (IPair<OldLeft, OldRight>) argumentPair; + + return argPair.mapRight(func); + }; + } + + /** + * Get the value on the left side of the pair + * + * @return The value on the left side of the pair + */ + @Override + public default LeftType getLeft() { + return merge((leftValue, rightValue) -> leftValue); + } + + /** + * Get the value on the right side of the pair + * + * @return The value on the right side of the pair + */ + @Override + public default RightType getRight() { + return merge((leftValue, rightValue) -> rightValue); + } + + /** + * Transform the value on the left side of the pair. Doesn't modify the + * pair + * + * @param <NewLeft> + * The new type of the left part of the pair + * @param mapper + * The function to use to transform the left part of the + * pair + * @return The pair, with its left part transformed + */ + public <NewLeft> IPair<NewLeft, RightType> mapLeft(Function<LeftType, NewLeft> mapper); + + /** + * Transform the value on the right side of the pair. Doesn't modify the + * pair + * + * @param <NewRight> + * The new type of the right part of the pair + * @param mapper + * The function to use to transform the right part of the + * pair + * @return The pair, with its right part transformed + */ + public <NewRight> IPair<LeftType, NewRight> mapRight(Function<RightType, NewRight> mapper); + + /** + * Merge the two values in this pair into a single value + * + * @param <MergedType> + * The type of the single value + * @param merger + * The function to use for merging + * @return The pair, merged into a single value + */ + public <MergedType> MergedType merge(BiFunction<LeftType, RightType, MergedType> merger); +} diff --git a/base/src/main/java/bjc/utils/data/ITree.java b/base/src/main/java/bjc/utils/data/ITree.java new file mode 100644 index 0000000..ff374e8 --- /dev/null +++ b/base/src/main/java/bjc/utils/data/ITree.java @@ -0,0 +1,234 @@ +package bjc.utils.data; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +import bjc.utils.funcdata.bst.TreeLinearizationMethod; +import bjc.utils.functypes.ListFlattener; + +/** + * A node in a homogeneous tree with a unlimited amount of children. + * + * @author ben + * + * @param <ContainedType> + * The type of data contained in the tree nodes. + * + */ +public interface ITree<ContainedType> { + /** + * Append a child to this node. + * + * @param child + * The child to append to this node. + */ + void addChild(ITree<ContainedType> child); + + /** + * Prepend a child to this node. + * + * @param child + * The child to prepend to this node. + */ + void prependChild(ITree<ContainedType> child); + + /** + * Collapse a tree into a single version. + * + * @param <NewType> + * The intermediate type being folded. + * + * @param <ReturnedType> + * The type that is the end result. + * + * @param leafTransform + * The function to use to convert leaf values. + * + * @param nodeCollapser + * The function to use to convert internal nodes and + * their children. + * + * @param resultTransformer + * The function to use to convert a state to the returned + * version. + * + * @return The final transformed state. + */ + <NewType, ReturnedType> ReturnedType collapse(Function<ContainedType, NewType> leafTransform, + Function<ContainedType, ListFlattener<NewType>> nodeCollapser, + Function<NewType, ReturnedType> resultTransformer); + + /** + * Execute a given action for each of this tree's children. + * + * @param action + * The action to execute for each child. + */ + void doForChildren(Consumer<ITree<ContainedType>> action); + + /** + * Expand the nodes of a tree into trees, and then merge the contents of + * those trees into a single tree. + * + * @param mapper + * The function to use to map values into trees. + * + * @return A tree, with some nodes expanded into trees. + */ + default ITree<ContainedType> flatMapTree(final Function<ContainedType, ITree<ContainedType>> mapper) { + return topDownTransform(dat -> TopDownTransformResult.PUSHDOWN, node -> { + if (node.getChildrenCount() > 0) { + final ITree<ContainedType> parent = node.transformHead(mapper); + + node.doForChildren(parent::addChild); + + return parent; + } + + return node.transformHead(mapper); + }); + } + + /** + * Get the specified child of this tree. + * + * @param childNo + * The number of the child to get. + * + * @return The specified child of this tree. + */ + default ITree<ContainedType> getChild(final int childNo) { + return transformChild(childNo, child -> child); + } + + /** + * Get a count of the number of direct children this node has. + * + * @return The number of direct children this node has. + */ + int getChildrenCount(); + + /** + * Get the data stored in this node. + * + * @return The data stored in this node. + */ + default ContainedType getHead() { + return transformHead(head -> head); + } + + /** + * Rebuild the tree with the same structure, but different nodes. + * + * @param <MappedType> + * The type of the new tree. + * + * @param leafTransformer + * The function to use to transform leaf tokens. + * + * @param operatorTransformer + * The function to use to transform internal tokens. + * + * @return The tree, with the nodes changed. + */ + <MappedType> ITree<MappedType> rebuildTree(Function<ContainedType, MappedType> leafTransformer, + Function<ContainedType, MappedType> operatorTransformer); + + /** + * Transform some of the nodes in this tree. + * + * @param nodePicker + * The predicate to use to pick nodes to transform. + * + * @param transformer + * The function to use to transform picked nodes. + */ + void selectiveTransform(Predicate<ContainedType> nodePicker, UnaryOperator<ContainedType> transformer); + + /** + * Do a top-down transform of the tree. + * + * @param transformPicker + * The function to use to pick how to progress. + * + * @param transformer + * The function used to transform picked subtrees. + * + * @return The tree with the transform applied to picked subtrees. + */ + ITree<ContainedType> topDownTransform(Function<ContainedType, TopDownTransformResult> transformPicker, + UnaryOperator<ITree<ContainedType>> transformer); + + /** + * Transform one of this nodes children. + * + * @param <TransformedType> + * The type of the transformed value. + * + * @param childNo + * The number of the child to transform. + * + * @param transformer + * The function to use to transform the value. + * + * @return The transformed value. + * + * @throws IllegalArgumentException + * if the childNo is out of bounds (0 <= childNo <= + * childCount()). + */ + <TransformedType> TransformedType transformChild(int childNo, + Function<ITree<ContainedType>, TransformedType> transformer); + + /** + * Transform the value that is the head of this node. + * + * @param <TransformedType> + * The type of the transformed value. + * + * @param transformer + * The function to use to transform the value. + * + * @return The transformed value. + */ + <TransformedType> TransformedType transformHead(Function<ContainedType, TransformedType> transformer); + + /** + * Transform the tree into a tree with a different type of token. + * + * @param <MappedType> + * The type of the new tree. + * + * @param transformer + * The function to use to transform tokens. + * + * @return A tree with the token types transformed. + */ + default <MappedType> ITree<MappedType> transformTree(final Function<ContainedType, MappedType> transformer) { + return rebuildTree(transformer, transformer); + } + + /** + * Perform an action on each part of the tree. + * + * @param linearizationMethod + * The way to traverse the tree. + * + * @param action + * The action to perform on each tree node. + */ + void traverse(TreeLinearizationMethod linearizationMethod, Consumer<ContainedType> action); + + /** + * Find the farthest to right child that satisfies the given predicate. + * + * @param childPred + * The predicate to satisfy. + * + * @return The index of the right-most child that satisfies the + * predicate, or -1 if one doesn't exist. + */ + int revFind(Predicate<ITree<ContainedType>> childPred); +} diff --git a/base/src/main/java/bjc/utils/data/Identity.java b/base/src/main/java/bjc/utils/data/Identity.java new file mode 100644 index 0000000..a8c8d70 --- /dev/null +++ b/base/src/main/java/bjc/utils/data/Identity.java @@ -0,0 +1,118 @@ +package bjc.utils.data; + +import java.util.function.Function; +import java.util.function.UnaryOperator; + +/** + * @author ben + * + * @param <ContainedType> + */ +/** + * Simple implementation of IHolder that has no hidden behavior + * + * @author ben + * + * @param <ContainedType> + * The type contained in the holder + */ +public class Identity<ContainedType> implements IHolder<ContainedType> { + private ContainedType heldValue; + + /** + * Create a holder holding null + */ + public Identity() { + heldValue = null; + } + + /** + * Create a holder holding the specified value + * + * @param value + * The value to hold + */ + public Identity(final ContainedType value) { + heldValue = value; + } + + @Override + public <BoundType> IHolder<BoundType> bind(final Function<ContainedType, IHolder<BoundType>> binder) { + return binder.apply(heldValue); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + + result = prime * result + (heldValue == null ? 0 : heldValue.hashCode()); + + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof Identity)) return false; + + final Identity<?> other = (Identity<?>) obj; + + if (heldValue == null) { + if (other.heldValue != null) return false; + } else if (!heldValue.equals(other.heldValue)) return false; + + return true; + } + + @Override + public <NewType> Function<ContainedType, IHolder<NewType>> lift(final Function<ContainedType, NewType> func) { + return (val) -> { + return new Identity<>(func.apply(val)); + }; + } + + @Override + public <MappedType> IHolder<MappedType> map(final Function<ContainedType, MappedType> mapper) { + return new Identity<>(mapper.apply(heldValue)); + } + + @Override + public String toString() { + return String.format("Identity [heldValue=%s]", heldValue); + } + + @Override + public IHolder<ContainedType> transform(final UnaryOperator<ContainedType> transformer) { + heldValue = transformer.apply(heldValue); + + return this; + } + + @Override + public <UnwrappedType> UnwrappedType unwrap(final Function<ContainedType, UnwrappedType> unwrapper) { + return unwrapper.apply(heldValue); + } + + /** + * Create a new identity container. + * + * @param val + * The contained value. + * + * @return A new identity container. + */ + public static <ContainedType> Identity<ContainedType> id(final ContainedType val) { + return new Identity<>(val); + } + + /** + * Create a new empty identity container. + * + * @return A new empty identity container. + */ + public static <ContainedType> Identity<ContainedType> id() { + return new Identity<>(); + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/data/Lazy.java b/base/src/main/java/bjc/utils/data/Lazy.java new file mode 100644 index 0000000..ca41b62 --- /dev/null +++ b/base/src/main/java/bjc/utils/data/Lazy.java @@ -0,0 +1,194 @@ +package bjc.utils.data; + +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; + +import bjc.utils.data.internals.BoundLazy; +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.IList; + +/** + * A holder that holds a means to create a value, but doesn't actually compute + * the value until it's needed + * + * @author ben + * + * @param <ContainedType> + */ +public class Lazy<ContainedType> implements IHolder<ContainedType> { + private Supplier<ContainedType> valueSupplier; + + private IList<UnaryOperator<ContainedType>> actions = new FunctionalList<>(); + + private boolean valueMaterialized; + + private ContainedType heldValue; + + /** + * Create a new lazy value from the specified seed value + * + * @param value + * The seed value to use + */ + public Lazy(final ContainedType value) { + heldValue = value; + + valueMaterialized = true; + } + + /** + * Create a new lazy value from the specified value source + * + * @param supp + * The source of a value to use + */ + public Lazy(final Supplier<ContainedType> supp) { + valueSupplier = new SingleSupplier<>(supp); + + valueMaterialized = false; + } + + private Lazy(final Supplier<ContainedType> supp, final IList<UnaryOperator<ContainedType>> pendingActions) { + valueSupplier = supp; + + actions = pendingActions; + } + + @Override + public <BoundType> IHolder<BoundType> bind(final Function<ContainedType, IHolder<BoundType>> binder) { + final IList<UnaryOperator<ContainedType>> pendingActions = new FunctionalList<>(); + + actions.forEach(pendingActions::add); + + final Supplier<ContainedType> supplier = () -> { + if (valueMaterialized) return heldValue; + + return valueSupplier.get(); + }; + + return new BoundLazy<>(() -> { + return new Lazy<>(supplier, pendingActions); + }, binder); + } + + @Override + public <NewType> Function<ContainedType, IHolder<NewType>> lift(final Function<ContainedType, NewType> func) { + return val -> { + return new Lazy<>(func.apply(val)); + }; + } + + @Override + public <MappedType> IHolder<MappedType> map(final Function<ContainedType, MappedType> mapper) { + final IList<UnaryOperator<ContainedType>> pendingActions = new FunctionalList<>(); + + actions.forEach(pendingActions::add); + + return new Lazy<>(() -> { + ContainedType currVal = heldValue; + + if (!valueMaterialized) { + currVal = valueSupplier.get(); + } + + return pendingActions.reduceAux(currVal, UnaryOperator<ContainedType>::apply, + value -> mapper.apply(value)); + }); + } + + @Override + public String toString() { + if (valueMaterialized) { + if (actions.isEmpty()) + return String.format("value[v='%s']", heldValue); + else return String.format("value[v='%s'] (has pending transforms)", heldValue); + } + + return "(unmaterialized)"; + } + + @Override + public IHolder<ContainedType> transform(final UnaryOperator<ContainedType> transformer) { + actions.add(transformer); + + return this; + } + + @Override + public <UnwrappedType> UnwrappedType unwrap(final Function<ContainedType, UnwrappedType> unwrapper) { + if (!valueMaterialized) { + heldValue = valueSupplier.get(); + + valueMaterialized = true; + } + + actions.forEach(action -> { + heldValue = action.apply(heldValue); + }); + + actions = new FunctionalList<>(); + + return unwrapper.apply(heldValue); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + + result = prime * result + (actions == null ? 0 : actions.hashCode()); + result = prime * result + (heldValue == null ? 0 : heldValue.hashCode()); + result = prime * result + (valueMaterialized ? 1231 : 1237); + + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof Lazy<?>)) return false; + + final Lazy<?> other = (Lazy<?>) obj; + + if (valueMaterialized != other.valueMaterialized) return false; + + if (valueMaterialized) { + if (heldValue == null) { + if (other.heldValue != null) return false; + } else if (!heldValue.equals(other.heldValue)) return false; + } else return false; + + if (actions == null) { + if (other.actions != null) return false; + } else if (actions.getSize() > 0 || other.actions.getSize() > 0) return false; + + return true; + } + + /** + * Create a new lazy container with an already present value. + * + * @param val + * The value for the lazy container. + * + * @return A new lazy container holding that value. + */ + public static <ContainedType> Lazy<ContainedType> lazy(final ContainedType val) { + return new Lazy<>(val); + } + + /** + * Create a new lazy container with a suspended value. + * + * @param supp + * The suspended value for the lazy container. + * + * @return A new lazy container that will un-suspend the value when + * necessary. + */ + public static <ContainedType> Lazy<ContainedType> lazy(final Supplier<ContainedType> supp) { + return new Lazy<>(supp); + } +} diff --git a/base/src/main/java/bjc/utils/data/LazyPair.java b/base/src/main/java/bjc/utils/data/LazyPair.java new file mode 100644 index 0000000..5cb85f3 --- /dev/null +++ b/base/src/main/java/bjc/utils/data/LazyPair.java @@ -0,0 +1,240 @@ +package bjc.utils.data; + +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +import bjc.utils.data.internals.BoundLazyPair; +import bjc.utils.data.internals.HalfBoundLazyPair; + +/** + * A lazy implementation of a pair + * + * @author ben + * + * @param <LeftType> + * The type on the left side of the pair + * @param <RightType> + * The type on the right side of the pair + * + */ +public class LazyPair<LeftType, RightType> implements IPair<LeftType, RightType> { + private LeftType leftValue; + private RightType rightValue; + + private Supplier<LeftType> leftSupplier; + private Supplier<RightType> rightSupplier; + + private boolean leftMaterialized; + private boolean rightMaterialized; + + /** + * Create a new lazy pair, using the set values + * + * @param leftVal + * The value for the left side of the pair + * @param rightVal + * The value for the right side of the pair + */ + public LazyPair(final LeftType leftVal, final RightType rightVal) { + leftValue = leftVal; + rightValue = rightVal; + + leftMaterialized = true; + rightMaterialized = true; + } + + /** + * Create a new lazy pair from the given value sources + * + * @param leftSupp + * The source for a value on the left side of the pair + * @param rightSupp + * The source for a value on the right side of the pair + */ + public LazyPair(final Supplier<LeftType> leftSupp, final Supplier<RightType> rightSupp) { + // Use single suppliers to catch double-instantiation bugs + leftSupplier = new SingleSupplier<>(leftSupp); + rightSupplier = new SingleSupplier<>(rightSupp); + + leftMaterialized = false; + rightMaterialized = false; + } + + @Override + public <BoundLeft, BoundRight> IPair<BoundLeft, BoundRight> bind( + final BiFunction<LeftType, RightType, IPair<BoundLeft, BoundRight>> binder) { + return new BoundLazyPair<>(leftSupplier, rightSupplier, binder); + } + + @Override + public <BoundLeft> IPair<BoundLeft, RightType> bindLeft( + final Function<LeftType, IPair<BoundLeft, RightType>> leftBinder) { + final Supplier<LeftType> leftSupp = () -> { + if (leftMaterialized) return leftValue; + + return leftSupplier.get(); + }; + + return new HalfBoundLazyPair<>(leftSupp, leftBinder); + } + + @Override + public <BoundRight> IPair<LeftType, BoundRight> bindRight( + final Function<RightType, IPair<LeftType, BoundRight>> rightBinder) { + final Supplier<RightType> rightSupp = () -> { + if (rightMaterialized) return rightValue; + + return rightSupplier.get(); + }; + + return new HalfBoundLazyPair<>(rightSupp, rightBinder); + } + + @Override + public <OtherLeft, OtherRight, CombinedLeft, CombinedRight> IPair<CombinedLeft, CombinedRight> combine( + final IPair<OtherLeft, OtherRight> otherPair, + final BiFunction<LeftType, OtherLeft, CombinedLeft> leftCombiner, + final BiFunction<RightType, OtherRight, CombinedRight> rightCombiner) { + return otherPair.bind((otherLeft, otherRight) -> { + return bind((leftVal, rightVal) -> { + final CombinedLeft left = leftCombiner.apply(leftVal, otherLeft); + final CombinedRight right = rightCombiner.apply(rightVal, otherRight); + + return new LazyPair<>(left, right); + }); + }); + } + + @Override + public LeftType getLeft() { + if (!leftMaterialized) { + leftValue = leftSupplier.get(); + + leftMaterialized = true; + } + + return leftValue; + } + + @Override + public RightType getRight() { + if (!rightMaterialized) { + rightValue = rightSupplier.get(); + + rightMaterialized = true; + } + + return rightValue; + } + + @Override + public <NewLeft> IPair<NewLeft, RightType> mapLeft(final Function<LeftType, NewLeft> mapper) { + final Supplier<NewLeft> leftSupp = () -> { + if (leftMaterialized) return mapper.apply(leftValue); + + return mapper.apply(leftSupplier.get()); + }; + + final Supplier<RightType> rightSupp = () -> { + if (rightMaterialized) return rightValue; + + return rightSupplier.get(); + }; + + return new LazyPair<>(leftSupp, rightSupp); + } + + @Override + public <NewRight> IPair<LeftType, NewRight> mapRight(final Function<RightType, NewRight> mapper) { + final Supplier<LeftType> leftSupp = () -> { + if (leftMaterialized) return leftValue; + + return leftSupplier.get(); + }; + + final Supplier<NewRight> rightSupp = () -> { + if (rightMaterialized) return mapper.apply(rightValue); + + return mapper.apply(rightSupplier.get()); + }; + + return new LazyPair<>(leftSupp, rightSupp); + } + + @Override + public <MergedType> MergedType merge(final BiFunction<LeftType, RightType, MergedType> merger) { + if (!leftMaterialized) { + leftValue = leftSupplier.get(); + + leftMaterialized = true; + } + + if (!rightMaterialized) { + rightValue = rightSupplier.get(); + + rightMaterialized = true; + } + + return merger.apply(leftValue, rightValue); + } + + @Override + public String toString() { + String leftVal; + String rightVal; + + if (leftMaterialized) { + leftVal = leftValue.toString(); + } else { + leftVal = "(un-materialized)"; + } + + if (rightMaterialized) { + rightVal = rightValue.toString(); + } else { + rightVal = "(un-materialized)"; + } + + return String.format("pair[l=%s,r=%s]", leftVal, rightVal); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + + result = prime * result + (leftMaterialized ? 1231 : 1237); + result = prime * result + (leftValue == null ? 0 : leftValue.hashCode()); + result = prime * result + (rightMaterialized ? 1231 : 1237); + result = prime * result + (rightValue == null ? 0 : rightValue.hashCode()); + + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof LazyPair<?, ?>)) return false; + + final LazyPair<?, ?> other = (LazyPair<?, ?>) obj; + + if (leftMaterialized != other.leftMaterialized) return false; + + if (leftMaterialized) { + if (leftValue == null) { + if (other.leftValue != null) return false; + } else if (!leftValue.equals(other.leftValue)) return false; + } else return false; + + if (rightMaterialized != other.rightMaterialized) return false; + if (rightMaterialized) { + if (rightValue == null) { + if (other.rightValue != null) return false; + } else if (!rightValue.equals(other.rightValue)) return false; + } else return false; + + return true; + } +} diff --git a/base/src/main/java/bjc/utils/data/ListHolder.java b/base/src/main/java/bjc/utils/data/ListHolder.java new file mode 100644 index 0000000..142057c --- /dev/null +++ b/base/src/main/java/bjc/utils/data/ListHolder.java @@ -0,0 +1,104 @@ +package bjc.utils.data; + +import java.util.function.Function; +import java.util.function.UnaryOperator; + +import bjc.utils.data.internals.BoundListHolder; +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.IList; + +/** + * A holder that represents a set of non-deterministic computations + * + * @author ben + * + * @param <ContainedType> + * The type of contained value + */ +public class ListHolder<ContainedType> implements IHolder<ContainedType> { + private IList<ContainedType> heldValues; + + /** + * Create a new list holder + * + * @param values + * The possible values for the computation + */ + @SafeVarargs + public ListHolder(final ContainedType... values) { + heldValues = new FunctionalList<>(); + + if (values != null) { + for (final ContainedType containedValue : values) { + heldValues.add(containedValue); + } + } + } + + private ListHolder(final IList<ContainedType> toHold) { + heldValues = toHold; + } + + @Override + public <BoundType> IHolder<BoundType> bind(final Function<ContainedType, IHolder<BoundType>> binder) { + final IList<IHolder<BoundType>> boundValues = heldValues.map(binder); + + return new BoundListHolder<>(boundValues); + } + + @Override + public <NewType> Function<ContainedType, IHolder<NewType>> lift(final Function<ContainedType, NewType> func) { + return val -> { + return new ListHolder<>(new FunctionalList<>(func.apply(val))); + }; + } + + @Override + public <MappedType> IHolder<MappedType> map(final Function<ContainedType, MappedType> mapper) { + final IList<MappedType> mappedValues = heldValues.map(mapper); + + return new ListHolder<>(mappedValues); + } + + @Override + public IHolder<ContainedType> transform(final UnaryOperator<ContainedType> transformer) { + heldValues = heldValues.map(transformer); + + return this; + } + + @Override + public <UnwrappedType> UnwrappedType unwrap(final Function<ContainedType, UnwrappedType> unwrapper) { + return unwrapper.apply(heldValues.randItem()); + } + + @Override + public String toString() { + return String.format("ListHolder [heldValues=%s]", heldValues); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + + result = prime * result + (heldValues == null ? 0 : heldValues.hashCode()); + + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof ListHolder<?>)) return false; + + final ListHolder<?> other = (ListHolder<?>) obj; + + if (heldValues == null) { + if (other.heldValues != null) return false; + } else if (!heldValues.equals(other.heldValues)) return false; + + return true; + } +} diff --git a/base/src/main/java/bjc/utils/data/Option.java b/base/src/main/java/bjc/utils/data/Option.java new file mode 100644 index 0000000..37e0cde --- /dev/null +++ b/base/src/main/java/bjc/utils/data/Option.java @@ -0,0 +1,93 @@ +package bjc.utils.data; + +import java.util.function.Function; +import java.util.function.UnaryOperator; + +/** + * A holder that may or may not contain a value + * + * @author ben + * + * @param <ContainedType> + * The type of the value that may or may not be held + */ +public class Option<ContainedType> implements IHolder<ContainedType> { + private ContainedType held; + + /** + * Create a new optional, using the given initial value + * + * @param seed + * The initial value for the optional + */ + public Option(final ContainedType seed) { + held = seed; + } + + @Override + public <BoundType> IHolder<BoundType> bind(final Function<ContainedType, IHolder<BoundType>> binder) { + if (held == null) return new Option<>(null); + + return binder.apply(held); + } + + @Override + public <NewType> Function<ContainedType, IHolder<NewType>> lift(final Function<ContainedType, NewType> func) { + return val -> { + return new Option<>(func.apply(val)); + }; + } + + @Override + public <MappedType> IHolder<MappedType> map(final Function<ContainedType, MappedType> mapper) { + if (held == null) return new Option<>(null); + + return new Option<>(mapper.apply(held)); + } + + @Override + public IHolder<ContainedType> transform(final UnaryOperator<ContainedType> transformer) { + if (held != null) { + held = transformer.apply(held); + } + + return this; + } + + @Override + public <UnwrappedType> UnwrappedType unwrap(final Function<ContainedType, UnwrappedType> unwrapper) { + if (held == null) return null; + + return unwrapper.apply(held); + } + + @Override + public String toString() { + return String.format("Option [held='%s']", held); + } + + @Override + public int hashCode() { + final int prime = 31; + + int result = 1; + result = prime * result + (held == null ? 0 : held.hashCode()); + + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof Option<?>)) return false; + + final Option<?> other = (Option<?>) obj; + + if (held == null) { + if (other.held != null) return false; + } else if (!held.equals(other.held)) return false; + + return true; + } +} diff --git a/base/src/main/java/bjc/utils/data/Pair.java b/base/src/main/java/bjc/utils/data/Pair.java new file mode 100644 index 0000000..e6796ba --- /dev/null +++ b/base/src/main/java/bjc/utils/data/Pair.java @@ -0,0 +1,135 @@ +package bjc.utils.data; + +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * A pair of values, with nothing special about them. + * + * @author ben + * + * @param <LeftType> + * The type of the left value + * @param <RightType> + * The type of the right value + */ +public class Pair<LeftType, RightType> implements IPair<LeftType, RightType> { + // The left value + private LeftType leftValue; + + // The right value + private RightType rightValue; + + /** + * Create a new pair with both sides set to null + */ + public Pair() { + + } + + /** + * Create a new pair with both sides set to the specified values + * + * @param left + * The value of the left side + * @param right + * The value of the right side + */ + public Pair(final LeftType left, final RightType right) { + leftValue = left; + rightValue = right; + } + + @Override + public <BoundLeft, BoundRight> IPair<BoundLeft, BoundRight> bind( + final BiFunction<LeftType, RightType, IPair<BoundLeft, BoundRight>> binder) { + if (binder == null) throw new NullPointerException("Binder must not be null."); + + return binder.apply(leftValue, rightValue); + } + + @Override + public <BoundLeft> IPair<BoundLeft, RightType> bindLeft( + final Function<LeftType, IPair<BoundLeft, RightType>> leftBinder) { + if (leftBinder == null) throw new NullPointerException("Binder must not be null"); + + return leftBinder.apply(leftValue); + } + + @Override + public <BoundRight> IPair<LeftType, BoundRight> bindRight( + final Function<RightType, IPair<LeftType, BoundRight>> rightBinder) { + if (rightBinder == null) throw new NullPointerException("Binder must not be null"); + + return rightBinder.apply(rightValue); + } + + @Override + public <OtherLeft, OtherRight, CombinedLeft, CombinedRight> IPair<CombinedLeft, CombinedRight> combine( + final IPair<OtherLeft, OtherRight> otherPair, + final BiFunction<LeftType, OtherLeft, CombinedLeft> leftCombiner, + final BiFunction<RightType, OtherRight, CombinedRight> rightCombiner) { + return otherPair.bind((otherLeft, otherRight) -> { + final CombinedLeft left = leftCombiner.apply(leftValue, otherLeft); + final CombinedRight right = rightCombiner.apply(rightValue, otherRight); + + return new Pair<>(left, right); + }); + } + + @Override + public <NewLeft> IPair<NewLeft, RightType> mapLeft(final Function<LeftType, NewLeft> mapper) { + if (mapper == null) throw new NullPointerException("Mapper must not be null"); + + return new Pair<>(mapper.apply(leftValue), rightValue); + } + + @Override + public <NewRight> IPair<LeftType, NewRight> mapRight(final Function<RightType, NewRight> mapper) { + if (mapper == null) throw new NullPointerException("Mapper must not be null"); + + return new Pair<>(leftValue, mapper.apply(rightValue)); + } + + @Override + public <MergedType> MergedType merge(final BiFunction<LeftType, RightType, MergedType> merger) { + if (merger == null) throw new NullPointerException("Merger must not be null"); + + return merger.apply(leftValue, rightValue); + } + + @Override + public String toString() { + return String.format("Pair [leftValue='%s', rightValue='%s']", leftValue, rightValue); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + + result = prime * result + (leftValue == null ? 0 : leftValue.hashCode()); + result = prime * result + (rightValue == null ? 0 : rightValue.hashCode()); + + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof Pair<?, ?>)) return false; + + final Pair<?, ?> other = (Pair<?, ?>) obj; + + if (leftValue == null) { + if (other.leftValue != null) return false; + } else if (!leftValue.equals(other.leftValue)) return false; + + if (rightValue == null) { + if (other.rightValue != null) return false; + } else if (!rightValue.equals(other.rightValue)) return false; + + return true; + } +} diff --git a/base/src/main/java/bjc/utils/data/SingleIterator.java b/base/src/main/java/bjc/utils/data/SingleIterator.java new file mode 100644 index 0000000..4069c3f --- /dev/null +++ b/base/src/main/java/bjc/utils/data/SingleIterator.java @@ -0,0 +1,41 @@ +package bjc.utils.data; + +import java.util.Iterator; + +/** + * An iterator that will only ever yield one item. + * + * @author EVE + * + * @param <T> + * The type of the item. + */ +public class SingleIterator<T> implements Iterator<T> { + private final T itm; + + private boolean yielded; + + /** + * Create a iterator that yields a single item. + * + * @param item + * The item to yield. + */ + public SingleIterator(final T item) { + itm = item; + + yielded = false; + } + + @Override + public boolean hasNext() { + return !yielded; + } + + @Override + public T next() { + yielded = true; + + return itm; + } +} diff --git a/base/src/main/java/bjc/utils/data/SingleSupplier.java b/base/src/main/java/bjc/utils/data/SingleSupplier.java new file mode 100644 index 0000000..c675ebf --- /dev/null +++ b/base/src/main/java/bjc/utils/data/SingleSupplier.java @@ -0,0 +1,72 @@ +package bjc.utils.data; + +import java.util.function.Supplier; + +/** + * A supplier that can only supply one value. + * + * Attempting to retrieve another value will cause an exception to be thrown. + * + * @author ben + * + * @param <T> + * The supplied type + */ +public class SingleSupplier<T> implements Supplier<T> { + private static long nextID = 0; + + private final Supplier<T> source; + + private boolean gotten; + + private final long id; + + /* + * This is bad practice, but I want to know where the single + * instantiation was, in case of duplicate initiations. + */ + private Exception instSite; + + /** + * Create a new single supplier from an existing value + * + * @param supp + * The supplier to give a single value from + */ + public SingleSupplier(final Supplier<T> supp) { + source = supp; + + gotten = false; + + id = nextID++; + } + + @Override + public T get() { + if (gotten == true) { + final String msg = String.format( + "Attempted to retrieve value more than once from single supplier #%d", id); + + final IllegalStateException isex = new IllegalStateException(msg); + + isex.initCause(instSite); + + throw isex; + } + + gotten = true; + + try { + throw new IllegalStateException("Previous instantiation here."); + } catch (final IllegalStateException isex) { + instSite = isex; + } + + return source.get(); + } + + @Override + public String toString() { + return String.format("SingleSupplier [source='%s', gotten=%s, id=%s]", source, gotten, id); + } +} diff --git a/base/src/main/java/bjc/utils/data/Toggle.java b/base/src/main/java/bjc/utils/data/Toggle.java new file mode 100644 index 0000000..1e10dae --- /dev/null +++ b/base/src/main/java/bjc/utils/data/Toggle.java @@ -0,0 +1,35 @@ +package bjc.utils.data; + +/** + * A stateful holder that swaps between two values of the same type. + * + * @author EVE + * + * @param <E> + * The value stored in the toggle. + */ +public interface Toggle<E> { + /** + * Retrieve the currently-aligned value of this toggle, and swap the + * alignment. + * + * @return The previously-aligned value. + */ + E get(); + + /** + * Retrieve the currently-aligned value without altering the alignment. + * + * @return The currently-aligned value. + */ + E peek(); + + /** + * Change the alignment of the toggle. + * + * @param isLeft + * Whether the toggle should be left-aligned or not. + */ + void set(boolean isLeft); + +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/data/TopDownTransformIterator.java b/base/src/main/java/bjc/utils/data/TopDownTransformIterator.java new file mode 100644 index 0000000..1b87e52 --- /dev/null +++ b/base/src/main/java/bjc/utils/data/TopDownTransformIterator.java @@ -0,0 +1,208 @@ +package bjc.utils.data; + +import static bjc.utils.data.TopDownTransformResult.RTRANSFORM; + +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.NoSuchElementException; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; + +/* + * FIXME something is broken in here. fix it. + */ +public class TopDownTransformIterator<ContainedType> implements Iterator<ITree<ContainedType>> { + private final Function<ContainedType, TopDownTransformResult> picker; + private final BiFunction<ITree<ContainedType>, Consumer<Iterator<ITree<ContainedType>>>, ITree<ContainedType>> transform; + + private ITree<ContainedType> preParent; + private ITree<ContainedType> postParent; + + private final Deque<ITree<ContainedType>> preChildren; + private final Deque<ITree<ContainedType>> postChildren; + + private TopDownTransformIterator<ContainedType> curChild; + + private boolean done; + private boolean initial; + + private final Deque<Iterator<ITree<ContainedType>>> toYield; + private Iterator<ITree<ContainedType>> curYield; + + public TopDownTransformIterator(final Function<ContainedType, TopDownTransformResult> pickr, + final BiFunction<ITree<ContainedType>, Consumer<Iterator<ITree<ContainedType>>>, ITree<ContainedType>> transfrm, + final ITree<ContainedType> tree) { + preParent = tree; + + preChildren = new LinkedList<>(); + postChildren = new LinkedList<>(); + toYield = new LinkedList<>(); + + picker = pickr; + transform = transfrm; + + done = false; + initial = true; + } + + public void addYield(final Iterator<ITree<ContainedType>> src) { + if (curYield != null) { + toYield.push(curYield); + } + + curYield = src; + } + + @Override + public boolean hasNext() { + return !done; + } + + public ITree<ContainedType> flushYields(final ITree<ContainedType> val) { + if (curYield != null) { + toYield.add(new SingleIterator<>(val)); + + if (curYield.hasNext()) + return curYield.next(); + else { + while (toYield.size() != 0 && !curYield.hasNext()) { + curYield = toYield.pop(); + } + + if (toYield.size() == 0 && !curYield.hasNext()) { + curYield = null; + return val; + } else return curYield.next(); + } + } else return val; + } + + @Override + public ITree<ContainedType> next() { + if (done) throw new NoSuchElementException(); + + if (curYield != null) { + if (curYield.hasNext()) + return curYield.next(); + else { + while (toYield.size() != 0 && !curYield.hasNext()) { + curYield = toYield.pop(); + } + + if (toYield.size() == 0 && !curYield.hasNext()) { + curYield = null; + } else return curYield.next(); + } + } + + if (initial) { + final TopDownTransformResult res = picker.apply(preParent.getHead()); + + switch (res) { + case PASSTHROUGH: + postParent = new Tree<>(preParent.getHead()); + + if (preParent.getChildrenCount() != 0) { + for (int i = 0; i < preParent.getChildrenCount(); i++) { + preChildren.add(preParent.getChild(i)); + } + + // Return whatever the first child is + break; + } else { + done = true; + return flushYields(postParent); + } + case SKIP: + done = true; + return flushYields(preParent); + case TRANSFORM: + done = true; + return flushYields(transform.apply(preParent, this::addYield)); + case RTRANSFORM: + preParent = transform.apply(preParent, this::addYield); + return flushYields(preParent); + case PUSHDOWN: + if (preParent.getChildrenCount() != 0) { + for (int i = 0; i < preParent.getChildrenCount(); i++) { + preChildren.add(preParent.getChild(i)); + } + + // Return whatever the first child is + break; + } else { + done = true; + return flushYields(transform.apply(new Tree<>(preParent.getHead()), + this::addYield)); + } + case PULLUP: + final ITree<ContainedType> intRes = transform.apply(preParent, this::addYield); + + postParent = new Tree<>(intRes.getHead()); + + if (intRes.getChildrenCount() != 0) { + for (int i = 0; i < intRes.getChildrenCount(); i++) { + preChildren.add(intRes.getChild(i)); + } + + // Return whatever the first child is + break; + } else { + done = true; + return flushYields(postParent); + } + default: + throw new IllegalArgumentException("Unknown result type " + res); + } + + if (res != RTRANSFORM) { + initial = false; + } + } + + if (curChild == null || !curChild.hasNext()) { + if (preChildren.size() != 0) { + curChild = new TopDownTransformIterator<>(picker, transform, preChildren.pop()); + + final ITree<ContainedType> res = curChild.next(); + System.out.println("\t\tTRACE: adding node " + res + " to children"); + postChildren.add(res); + + return flushYields(res); + } else { + ITree<ContainedType> res = null; + + if (postParent == null) { + res = new Tree<>(preParent.getHead()); + + System.out.println("\t\tTRACE: adding nodes " + postChildren + " to " + res); + + for (final ITree<ContainedType> child : postChildren) { + res.addChild(child); + } + + // res = transform.apply(res, + // this::addYield); + } else { + res = postParent; + + System.out.println("\t\tTRACE: adding nodes " + postChildren + " to " + res); + for (final ITree<ContainedType> child : postChildren) { + res.addChild(child); + } + } + + done = true; + return flushYields(res); + } + } else { + final ITree<ContainedType> res = curChild.next(); + System.out.println("\t\tTRACE: adding node " + res + " to children"); + postChildren.add(res); + + return flushYields(res); + } + } +} diff --git a/base/src/main/java/bjc/utils/data/TopDownTransformResult.java b/base/src/main/java/bjc/utils/data/TopDownTransformResult.java new file mode 100644 index 0000000..ed41eae --- /dev/null +++ b/base/src/main/java/bjc/utils/data/TopDownTransformResult.java @@ -0,0 +1,34 @@ +package bjc.utils.data; + +/** + * Represents the results for doing a top-down transform of a tree + * + * @author ben + * + */ +public enum TopDownTransformResult { + /** + * Do not do anything to this node, and ignore its children + */ + SKIP, + /** + * Transform this node, and don't touch its children + */ + TRANSFORM, + /** + * Transform this node, then do a top-down transform on the result + */ + RTRANSFORM, + /** + * Ignore this node, and traverse its children + */ + PASSTHROUGH, + /** + * Traverse the nodes of this children, then transform it + */ + PUSHDOWN, + /** + * Transform this node, then traverse its children + */ + PULLUP; +} diff --git a/base/src/main/java/bjc/utils/data/TransformIterator.java b/base/src/main/java/bjc/utils/data/TransformIterator.java new file mode 100644 index 0000000..50f28b1 --- /dev/null +++ b/base/src/main/java/bjc/utils/data/TransformIterator.java @@ -0,0 +1,46 @@ +package bjc.utils.data; + +import java.util.Iterator; +import java.util.function.Function; + +/** + * An iterator that transforms values from one type to another. + * + * @author EVE + * + * @param <S> + * The source iterator type. + * + * @param <D> + * The destination iterator type. + */ +public class TransformIterator<S, D> implements Iterator<D> { + private final Iterator<S> source; + + private final Function<S, D> transform; + + /** + * Create a new transform iterator. + * + * @param source + * The source iterator to use. + * + * @param transform + * The transform to apply. + */ + public TransformIterator(final Iterator<S> source, final Function<S, D> transform) { + this.source = source; + this.transform = transform; + } + + @Override + public boolean hasNext() { + return source.hasNext(); + } + + @Override + public D next() { + return transform.apply(source.next()); + } + +} diff --git a/base/src/main/java/bjc/utils/data/Tree.java b/base/src/main/java/bjc/utils/data/Tree.java new file mode 100644 index 0000000..a52f699 --- /dev/null +++ b/base/src/main/java/bjc/utils/data/Tree.java @@ -0,0 +1,390 @@ +package bjc.utils.data; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.IList; +import bjc.utils.funcdata.bst.TreeLinearizationMethod; +import bjc.utils.functypes.ListFlattener; + +/** + * A node in a homogeneous tree. + * + * @author ben + * + * @param <ContainedType> + */ +public class Tree<ContainedType> implements ITree<ContainedType> { + private ContainedType data; + + private IList<ITree<ContainedType>> children; + private boolean hasChildren; + private int childCount = 0; + + private int ID; + private static int nextID = 0; + + /** + * Create a new leaf node in a tree. + * + * @param leaf + * The data to store as a leaf node. + */ + public Tree(final ContainedType leaf) { + data = leaf; + + hasChildren = false; + + ID = nextID++; + } + + /** + * Create a new tree node with the specified children. + * + * @param leaf + * The data to hold in this node. + * + * @param childrn + * A list of children for this node. + */ + public Tree(final ContainedType leaf, final IList<ITree<ContainedType>> childrn) { + this(leaf); + + hasChildren = true; + + childCount = childrn.getSize(); + + children = childrn; + } + + /** + * Create a new tree node with the specified children. + * + * @param leaf + * The data to hold in this node. + * + * @param childrn + * A list of children for this node. + */ + @SafeVarargs + public Tree(final ContainedType leaf, final ITree<ContainedType>... childrn) { + this(leaf); + + hasChildren = true; + + childCount = 0; + + children = new FunctionalList<>(); + + for (final ITree<ContainedType> child : childrn) { + children.add(child); + + childCount++; + } + } + + @Override + public void addChild(final ITree<ContainedType> child) { + if (hasChildren == false) { + hasChildren = true; + + children = new FunctionalList<>(); + } + + childCount++; + + children.add(child); + } + + @Override + public void prependChild(final ITree<ContainedType> child) { + if (hasChildren == false) { + hasChildren = true; + + children = new FunctionalList<>(); + } + + childCount++; + + children.prepend(child); + } + + @Override + public void doForChildren(final Consumer<ITree<ContainedType>> action) { + if (childCount > 0) { + children.forEach(action); + } + } + + @Override + public int getChildrenCount() { + return childCount; + } + + @Override + public int revFind(final Predicate<ITree<ContainedType>> childPred) { + if (childCount == 0) + return -1; + else { + for (int i = childCount - 1; i >= 0; i--) { + if (childPred.test(getChild(i))) return i; + } + } + + return -1; + } + + @Override + public void traverse(final TreeLinearizationMethod linearizationMethod, final Consumer<ContainedType> action) { + if (hasChildren) { + switch (linearizationMethod) { + case INORDER: + if (childCount != 2) { + final String msg = "Can only do in-order traversal for binary trees."; + + throw new IllegalArgumentException(msg); + } + + children.getByIndex(0).traverse(linearizationMethod, action); + + action.accept(data); + + children.getByIndex(1).traverse(linearizationMethod, action); + break; + case POSTORDER: + children.forEach((child) -> child.traverse(linearizationMethod, action)); + + action.accept(data); + break; + case PREORDER: + action.accept(data); + + children.forEach((child) -> child.traverse(linearizationMethod, action)); + break; + default: + break; + + } + } else { + action.accept(data); + } + } + + @Override + public <NewType, ReturnedType> ReturnedType collapse(final Function<ContainedType, NewType> leafTransform, + final Function<ContainedType, ListFlattener<NewType>> nodeCollapser, + final Function<NewType, ReturnedType> resultTransformer) { + return resultTransformer.apply(internalCollapse(leafTransform, nodeCollapser)); + } + + @Override + public ITree<ContainedType> flatMapTree(final Function<ContainedType, ITree<ContainedType>> mapper) { + if (hasChildren) { + final ITree<ContainedType> flatMappedData = mapper.apply(data); + + final IList<ITree<ContainedType>> mappedChildren = children + .map(child -> child.flatMapTree(mapper)); + + mappedChildren.forEach(child -> flatMappedData.addChild(child)); + + return flatMappedData; + } + + return mapper.apply(data); + } + + protected <NewType> NewType internalCollapse(final Function<ContainedType, NewType> leafTransform, + final Function<ContainedType, ListFlattener<NewType>> nodeCollapser) { + if (hasChildren) { + final Function<IList<NewType>, NewType> nodeTransformer = nodeCollapser.apply(data); + + final IList<NewType> collapsedChildren = children.map(child -> { + final NewType collapsed = child.collapse(leafTransform, nodeCollapser, + subTreeVal -> subTreeVal); + + return collapsed; + }); + + return nodeTransformer.apply(collapsedChildren); + } + + return leafTransform.apply(data); + } + + protected void internalToString(final StringBuilder builder, final int indentLevel, final boolean initial) { + for (int i = 0; i < indentLevel; i++) { + builder.append(">\t"); + } + + builder.append("Node #"); + builder.append(ID); + builder.append(": "); + builder.append(data == null ? "(null)" : data.toString()); + builder.append("\n"); + + if (hasChildren) { + children.forEach(child -> { + if (child instanceof Tree<?>) { + final Tree<ContainedType> kid = (Tree<ContainedType>) child; + + kid.internalToString(builder, indentLevel + 1, false); + } else { + for (int i = 0; i < indentLevel + 1; i++) { + builder.append(">\t"); + } + + builder.append("Unknown node\n"); + } + }); + } + } + + @Override + public <MappedType> ITree<MappedType> rebuildTree(final Function<ContainedType, MappedType> leafTransformer, + final Function<ContainedType, MappedType> operatorTransformer) { + if (hasChildren) { + final IList<ITree<MappedType>> mappedChildren = children.map(child -> { + return child.rebuildTree(leafTransformer, operatorTransformer); + }); + + return new Tree<>(operatorTransformer.apply(data), mappedChildren); + } + + return new Tree<>(leafTransformer.apply(data)); + } + + @Override + public void selectiveTransform(final Predicate<ContainedType> nodePicker, + final UnaryOperator<ContainedType> transformer) { + if (hasChildren) { + children.forEach(child -> child.selectiveTransform(nodePicker, transformer)); + } else { + data = transformer.apply(data); + } + } + + @Override + public ITree<ContainedType> topDownTransform( + final Function<ContainedType, TopDownTransformResult> transformPicker, + final UnaryOperator<ITree<ContainedType>> transformer) { + final TopDownTransformResult transformResult = transformPicker.apply(data); + + switch (transformResult) { + case PASSTHROUGH: + ITree<ContainedType> result = new Tree<>(data); + + if (hasChildren) { + children.forEach(child -> { + final ITree<ContainedType> kid = child.topDownTransform(transformPicker, + transformer); + + result.addChild(kid); + }); + } + + return result; + case SKIP: + return this; + case TRANSFORM: + return transformer.apply(this); + case RTRANSFORM: + return transformer.apply(this).topDownTransform(transformPicker, transformer); + case PUSHDOWN: + result = new Tree<>(data); + + if (hasChildren) { + children.forEach(child -> { + final ITree<ContainedType> kid = child.topDownTransform(transformPicker, + transformer); + + result.addChild(kid); + }); + } + + return transformer.apply(result); + case PULLUP: + final ITree<ContainedType> intermediateResult = transformer.apply(this); + + result = new Tree<>(intermediateResult.getHead()); + + intermediateResult.doForChildren(child -> { + final ITree<ContainedType> kid = child.topDownTransform(transformPicker, transformer); + + result.addChild(kid); + }); + + return result; + default: + final String msg = String.format("Recieved unknown transform result %s", transformResult); + + throw new IllegalArgumentException(msg); + } + } + + @Override + public <TransformedType> TransformedType transformChild(final int childNo, + final Function<ITree<ContainedType>, TransformedType> transformer) { + if (childNo < 0 || childNo > childCount - 1) { + final String msg = String.format("Child index #%d is invalid", childNo); + + throw new IllegalArgumentException(msg); + } + + final ITree<ContainedType> selectedKid = children.getByIndex(childNo); + + return transformer.apply(selectedKid); + } + + @Override + public <TransformedType> TransformedType transformHead( + final Function<ContainedType, TransformedType> transformer) { + return transformer.apply(data); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + + result = prime * result + childCount; + result = prime * result + (children == null ? 0 : children.hashCode()); + result = prime * result + (data == null ? 0 : data.hashCode()); + + return result; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + + internalToString(builder, 1, true); + + builder.deleteCharAt(builder.length() - 1); + + return builder.toString(); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof Tree<?>)) return false; + + final Tree<?> other = (Tree<?>) obj; + + if (data == null) { + if (other.data != null) return false; + } else if (!data.equals(other.data)) return false; + + if (childCount != other.childCount) return false; + + if (children == null) { + if (other.children != null) return false; + } else if (!children.equals(other.children)) return false; + + return true; + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/data/ValueToggle.java b/base/src/main/java/bjc/utils/data/ValueToggle.java new file mode 100644 index 0000000..9193896 --- /dev/null +++ b/base/src/main/java/bjc/utils/data/ValueToggle.java @@ -0,0 +1,54 @@ +package bjc.utils.data; + +/** + * A simple implementation of {@link Toggle}. + * + * @author EVE + * + * @param <E> + * The type of value to toggle between. + */ +public class ValueToggle<E> implements Toggle<E> { + private final E lft; + private final E rght; + + private final BooleanToggle alignment; + + /** + * Create a new toggle. + * + * All toggles start right-aligned. + * + * @param left + * The value when the toggle is left-aligned. + * + * @param right + * The value when the toggle is right-aligned. + */ + public ValueToggle(final E left, final E right) { + lft = left; + + rght = right; + + alignment = new BooleanToggle(); + } + + @Override + public E get() { + if (alignment.get()) + return lft; + else return rght; + } + + @Override + public E peek() { + if (alignment.peek()) + return lft; + else return rght; + } + + @Override + public void set(final boolean isLeft) { + alignment.set(isLeft); + } +} diff --git a/base/src/main/java/bjc/utils/data/internals/BoundLazy.java b/base/src/main/java/bjc/utils/data/internals/BoundLazy.java new file mode 100644 index 0000000..f71d32b --- /dev/null +++ b/base/src/main/java/bjc/utils/data/internals/BoundLazy.java @@ -0,0 +1,145 @@ +package bjc.utils.data.internals; + +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; + +import bjc.utils.data.IHolder; +import bjc.utils.data.Lazy; +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.IList; + +/* + * Implements a lazy holder that has been bound + */ +public class BoundLazy<OldType, BoundContainedType> implements IHolder<BoundContainedType> { + /* + * The old value + */ + private final Supplier<IHolder<OldType>> oldSupplier; + + /* + * The function to use to transform the old value into a new value + */ + private final Function<OldType, IHolder<BoundContainedType>> binder; + + /* + * The bound value being held + */ + private IHolder<BoundContainedType> boundHolder; + + /* + * Whether the bound value has been actualized or not + */ + private boolean holderBound; + + /* + * Transformations currently pending on the bound value + */ + private final IList<UnaryOperator<BoundContainedType>> actions = new FunctionalList<>(); + + /* + * Create a new bound lazy value + */ + public BoundLazy(final Supplier<IHolder<OldType>> supp, + final Function<OldType, IHolder<BoundContainedType>> binder) { + oldSupplier = supp; + this.binder = binder; + } + + @Override + public <BoundType> IHolder<BoundType> bind(final Function<BoundContainedType, IHolder<BoundType>> bindr) { + if (bindr == null) throw new NullPointerException("Binder must not be null"); + + /* + * Prepare a list of pending actions + */ + final IList<UnaryOperator<BoundContainedType>> pendingActions = new FunctionalList<>(); + actions.forEach(pendingActions::add); + + /* + * Create the new supplier of a value + */ + final Supplier<IHolder<BoundContainedType>> typeSupplier = () -> { + IHolder<BoundContainedType> oldHolder = boundHolder; + + /* + * Bind the value if it hasn't been bound before + */ + if (!holderBound) { + oldHolder = oldSupplier.get().unwrap(binder); + } + + /* + * Apply all the pending actions + */ + return pendingActions.reduceAux(oldHolder, (action, state) -> { + return state.transform(action); + }, (value) -> value); + }; + + return new BoundLazy<>(typeSupplier, bindr); + } + + @Override + public <NewType> Function<BoundContainedType, IHolder<NewType>> lift( + final Function<BoundContainedType, NewType> func) { + if (func == null) throw new NullPointerException("Function to lift must not be null"); + + return (val) -> { + return new Lazy<>(func.apply(val)); + }; + } + + @Override + public <MappedType> IHolder<MappedType> map(final Function<BoundContainedType, MappedType> mapper) { + if (mapper == null) throw new NullPointerException("Mapper must not be null"); + + // Prepare a list of pending actions + final IList<UnaryOperator<BoundContainedType>> pendingActions = new FunctionalList<>(); + actions.forEach(pendingActions::add); + + // Prepare the new supplier + final Supplier<MappedType> typeSupplier = () -> { + IHolder<BoundContainedType> oldHolder = boundHolder; + + // Bound the value if it hasn't been bound + if (!holderBound) { + oldHolder = oldSupplier.get().unwrap(binder); + } + + return pendingActions.reduceAux(oldHolder.getValue(), (action, state) -> { + return action.apply(state); + }, (value) -> mapper.apply(value)); + }; + + return new Lazy<>(typeSupplier); + } + + @Override + public String toString() { + if (holderBound) return boundHolder.toString(); + + return "(unmaterialized)"; + } + + @Override + public IHolder<BoundContainedType> transform(final UnaryOperator<BoundContainedType> transformer) { + if (transformer == null) throw new NullPointerException("Transformer must not be null"); + + actions.add(transformer); + + return this; + } + + @Override + public <UnwrappedType> UnwrappedType unwrap(final Function<BoundContainedType, UnwrappedType> unwrapper) { + if (unwrapper == null) throw new NullPointerException("Unwrapper must not be null"); + + if (!holderBound) { + boundHolder = oldSupplier.get().unwrap(binder::apply); + } + + return boundHolder.unwrap(unwrapper); + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/data/internals/BoundLazyPair.java b/base/src/main/java/bjc/utils/data/internals/BoundLazyPair.java new file mode 100644 index 0000000..df6e60b --- /dev/null +++ b/base/src/main/java/bjc/utils/data/internals/BoundLazyPair.java @@ -0,0 +1,199 @@ +package bjc.utils.data.internals; + +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +import bjc.utils.data.IHolder; +import bjc.utils.data.IPair; +import bjc.utils.data.Identity; +import bjc.utils.data.LazyPair; + +/* + * Implements a lazy pair that has been bound + */ +public class BoundLazyPair<OldLeft, OldRight, NewLeft, NewRight> implements IPair<NewLeft, NewRight> { + /* + * The supplier of the left value + */ + private final Supplier<OldLeft> leftSupplier; + /* + * The supplier of the right value + */ + private final Supplier<OldRight> rightSupplier; + + /* + * The binder to transform values + */ + private final BiFunction<OldLeft, OldRight, IPair<NewLeft, NewRight>> binder; + + /* + * The bound pair + */ + private IPair<NewLeft, NewRight> boundPair; + + /* + * Whether the pair has been bound yet + */ + private boolean pairBound; + + public BoundLazyPair(final Supplier<OldLeft> leftSupp, final Supplier<OldRight> rightSupp, + final BiFunction<OldLeft, OldRight, IPair<NewLeft, NewRight>> bindr) { + leftSupplier = leftSupp; + rightSupplier = rightSupp; + binder = bindr; + } + + @Override + public <BoundLeft, BoundRight> IPair<BoundLeft, BoundRight> bind( + final BiFunction<NewLeft, NewRight, IPair<BoundLeft, BoundRight>> bindr) { + if (bindr == null) throw new NullPointerException("Binder must not be null"); + + final IHolder<IPair<NewLeft, NewRight>> newPair = new Identity<>(boundPair); + final IHolder<Boolean> newPairMade = new Identity<>(pairBound); + + final Supplier<NewLeft> leftSupp = () -> { + if (!newPairMade.getValue()) { + newPair.replace(binder.apply(leftSupplier.get(), rightSupplier.get())); + + newPairMade.replace(true); + } + + return newPair.unwrap((pair) -> pair.getLeft()); + }; + + final Supplier<NewRight> rightSupp = () -> { + if (!newPairMade.getValue()) { + newPair.replace(binder.apply(leftSupplier.get(), rightSupplier.get())); + + newPairMade.replace(true); + } + + return newPair.unwrap((pair) -> pair.getRight()); + }; + + return new BoundLazyPair<>(leftSupp, rightSupp, bindr); + } + + @Override + public <BoundLeft> IPair<BoundLeft, NewRight> bindLeft( + final Function<NewLeft, IPair<BoundLeft, NewRight>> leftBinder) { + if (leftBinder == null) throw new NullPointerException("Left binder must not be null"); + + final Supplier<NewLeft> leftSupp = () -> { + IPair<NewLeft, NewRight> newPair = boundPair; + + if (!pairBound) { + newPair = binder.apply(leftSupplier.get(), rightSupplier.get()); + } + + return newPair.getLeft(); + }; + + return new HalfBoundLazyPair<>(leftSupp, leftBinder); + } + + @Override + public <BoundRight> IPair<NewLeft, BoundRight> bindRight( + final Function<NewRight, IPair<NewLeft, BoundRight>> rightBinder) { + if (rightBinder == null) throw new NullPointerException("Right binder must not be null"); + + final Supplier<NewRight> rightSupp = () -> { + IPair<NewLeft, NewRight> newPair = boundPair; + + if (!pairBound) { + newPair = binder.apply(leftSupplier.get(), rightSupplier.get()); + } + + return newPair.getRight(); + }; + + return new HalfBoundLazyPair<>(rightSupp, rightBinder); + } + + @Override + public <OtherLeft, OtherRight, CombinedLeft, CombinedRight> IPair<CombinedLeft, CombinedRight> combine( + final IPair<OtherLeft, OtherRight> otherPair, + final BiFunction<NewLeft, OtherLeft, CombinedLeft> leftCombiner, + final BiFunction<NewRight, OtherRight, CombinedRight> rightCombiner) { + if (otherPair == null) + throw new NullPointerException("Other pair must not be null"); + else if (leftCombiner == null) + throw new NullPointerException("Left combiner must not be null"); + else if (rightCombiner == null) throw new NullPointerException("Right combiner must not be null"); + + return otherPair.bind((otherLeft, otherRight) -> { + return bind((leftVal, rightVal) -> { + return new LazyPair<>(leftCombiner.apply(leftVal, otherLeft), + rightCombiner.apply(rightVal, otherRight)); + }); + }); + } + + @Override + public <NewLeftType> IPair<NewLeftType, NewRight> mapLeft(final Function<NewLeft, NewLeftType> mapper) { + if (mapper == null) throw new NullPointerException("Mapper must not be null"); + + final Supplier<NewLeftType> leftSupp = () -> { + if (!pairBound) { + final NewLeft leftVal = binder.apply(leftSupplier.get(), rightSupplier.get()).getLeft(); + + return mapper.apply(leftVal); + } + + return mapper.apply(boundPair.getLeft()); + }; + + final Supplier<NewRight> rightSupp = () -> { + if (!pairBound) return binder.apply(leftSupplier.get(), rightSupplier.get()).getRight(); + + return boundPair.getRight(); + }; + + return new LazyPair<>(leftSupp, rightSupp); + } + + @Override + public <NewRightType> IPair<NewLeft, NewRightType> mapRight(final Function<NewRight, NewRightType> mapper) { + if (mapper == null) throw new NullPointerException("Mapper must not be null"); + + final Supplier<NewLeft> leftSupp = () -> { + if (!pairBound) return binder.apply(leftSupplier.get(), rightSupplier.get()).getLeft(); + + return boundPair.getLeft(); + }; + + final Supplier<NewRightType> rightSupp = () -> { + if (!pairBound) { + final NewRight rightVal = binder.apply(leftSupplier.get(), rightSupplier.get()) + .getRight(); + + return mapper.apply(rightVal); + } + + return mapper.apply(boundPair.getRight()); + }; + + return new LazyPair<>(leftSupp, rightSupp); + } + + @Override + public <MergedType> MergedType merge(final BiFunction<NewLeft, NewRight, MergedType> merger) { + if (merger == null) throw new NullPointerException("Merger must not be null"); + + if (!pairBound) { + boundPair = binder.apply(leftSupplier.get(), rightSupplier.get()); + + pairBound = true; + } + + return boundPair.merge(merger); + } + + @Override + public String toString() { + if (pairBound) return boundPair.toString(); + + return "(un-materialized)"; + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/data/internals/BoundListHolder.java b/base/src/main/java/bjc/utils/data/internals/BoundListHolder.java new file mode 100644 index 0000000..f3799fd --- /dev/null +++ b/base/src/main/java/bjc/utils/data/internals/BoundListHolder.java @@ -0,0 +1,68 @@ +package bjc.utils.data.internals; + +import java.util.function.Function; +import java.util.function.UnaryOperator; + +import bjc.utils.data.IHolder; +import bjc.utils.data.ListHolder; +import bjc.utils.funcdata.IList; + +/* + * Holds a list, converted into a holder + */ +public class BoundListHolder<ContainedType> implements IHolder<ContainedType> { + private final IList<IHolder<ContainedType>> heldHolders; + + public BoundListHolder(final IList<IHolder<ContainedType>> toHold) { + heldHolders = toHold; + } + + @Override + public <BoundType> IHolder<BoundType> bind(final Function<ContainedType, IHolder<BoundType>> binder) { + if (binder == null) throw new NullPointerException("Binder must not be null"); + + final IList<IHolder<BoundType>> boundHolders = heldHolders.map((containedHolder) -> { + return containedHolder.bind(binder); + }); + + return new BoundListHolder<>(boundHolders); + } + + @Override + public <NewType> Function<ContainedType, IHolder<NewType>> lift(final Function<ContainedType, NewType> func) { + if (func == null) throw new NullPointerException("Function to lift must not be null"); + + return (val) -> { + return new ListHolder<>(func.apply(val)); + }; + } + + @Override + public <MappedType> IHolder<MappedType> map(final Function<ContainedType, MappedType> mapper) { + if (mapper == null) throw new NullPointerException("Mapper must not be null"); + + final IList<IHolder<MappedType>> mappedHolders = heldHolders.map((containedHolder) -> { + return containedHolder.map(mapper); + }); + + return new BoundListHolder<>(mappedHolders); + } + + @Override + public IHolder<ContainedType> transform(final UnaryOperator<ContainedType> transformer) { + if (transformer == null) throw new NullPointerException("Transformer must not be null"); + + heldHolders.forEach((containedHolder) -> { + containedHolder.transform(transformer); + }); + + return this; + } + + @Override + public <UnwrappedType> UnwrappedType unwrap(final Function<ContainedType, UnwrappedType> unwrapper) { + if (unwrapper == null) throw new NullPointerException("Unwrapper must not be null"); + + return heldHolders.randItem().unwrap(unwrapper); + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/data/internals/HalfBoundLazyPair.java b/base/src/main/java/bjc/utils/data/internals/HalfBoundLazyPair.java new file mode 100644 index 0000000..8cac38b --- /dev/null +++ b/base/src/main/java/bjc/utils/data/internals/HalfBoundLazyPair.java @@ -0,0 +1,149 @@ +package bjc.utils.data.internals; + +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +import bjc.utils.data.IHolder; +import bjc.utils.data.IPair; +import bjc.utils.data.Identity; +import bjc.utils.data.LazyPair; + +/* + * A lazy pair, with only one side bound + */ +public class HalfBoundLazyPair<OldType, NewLeft, NewRight> implements IPair<NewLeft, NewRight> { + private final Supplier<OldType> oldSupplier; + + private final Function<OldType, IPair<NewLeft, NewRight>> binder; + + private IPair<NewLeft, NewRight> boundPair; + private boolean pairBound; + + public HalfBoundLazyPair(final Supplier<OldType> oldSupp, + final Function<OldType, IPair<NewLeft, NewRight>> bindr) { + oldSupplier = oldSupp; + binder = bindr; + } + + @Override + public <BoundLeft, BoundRight> IPair<BoundLeft, BoundRight> bind( + final BiFunction<NewLeft, NewRight, IPair<BoundLeft, BoundRight>> bindr) { + final IHolder<IPair<NewLeft, NewRight>> newPair = new Identity<>(boundPair); + final IHolder<Boolean> newPairMade = new Identity<>(pairBound); + + final Supplier<NewLeft> leftSupp = () -> { + if (!newPairMade.getValue()) { + newPair.replace(binder.apply(oldSupplier.get())); + newPairMade.replace(true); + } + + return newPair.unwrap((pair) -> pair.getLeft()); + }; + + final Supplier<NewRight> rightSupp = () -> { + if (!newPairMade.getValue()) { + newPair.replace(binder.apply(oldSupplier.get())); + newPairMade.replace(true); + } + + return newPair.unwrap((pair) -> pair.getRight()); + }; + + return new BoundLazyPair<>(leftSupp, rightSupp, bindr); + } + + @Override + public <BoundLeft> IPair<BoundLeft, NewRight> bindLeft( + final Function<NewLeft, IPair<BoundLeft, NewRight>> leftBinder) { + final Supplier<NewLeft> leftSupp = () -> { + IPair<NewLeft, NewRight> newPair = boundPair; + + if (!pairBound) { + newPair = binder.apply(oldSupplier.get()); + } + + return newPair.getLeft(); + }; + + return new HalfBoundLazyPair<>(leftSupp, leftBinder); + } + + @Override + public <BoundRight> IPair<NewLeft, BoundRight> bindRight( + final Function<NewRight, IPair<NewLeft, BoundRight>> rightBinder) { + final Supplier<NewRight> rightSupp = () -> { + IPair<NewLeft, NewRight> newPair = boundPair; + + if (!pairBound) { + newPair = binder.apply(oldSupplier.get()); + } + + return newPair.getRight(); + }; + + return new HalfBoundLazyPair<>(rightSupp, rightBinder); + } + + @Override + public <OtherLeft, OtherRight, CombinedLeft, CombinedRight> IPair<CombinedLeft, CombinedRight> combine( + final IPair<OtherLeft, OtherRight> otherPair, + final BiFunction<NewLeft, OtherLeft, CombinedLeft> leftCombiner, + final BiFunction<NewRight, OtherRight, CombinedRight> rightCombiner) { + return otherPair.bind((otherLeft, otherRight) -> { + return bind((leftVal, rightVal) -> { + return new LazyPair<>(leftCombiner.apply(leftVal, otherLeft), + rightCombiner.apply(rightVal, otherRight)); + }); + }); + } + + @Override + public <NewLeftType> IPair<NewLeftType, NewRight> mapLeft(final Function<NewLeft, NewLeftType> mapper) { + final Supplier<NewLeftType> leftSupp = () -> { + if (pairBound) return mapper.apply(boundPair.getLeft()); + + final NewLeft leftVal = binder.apply(oldSupplier.get()).getLeft(); + + return mapper.apply(leftVal); + }; + + final Supplier<NewRight> rightSupp = () -> { + if (pairBound) return boundPair.getRight(); + + return binder.apply(oldSupplier.get()).getRight(); + }; + + return new LazyPair<>(leftSupp, rightSupp); + } + + @Override + public <NewRightType> IPair<NewLeft, NewRightType> mapRight(final Function<NewRight, NewRightType> mapper) { + final Supplier<NewLeft> leftSupp = () -> { + if (pairBound) return boundPair.getLeft(); + + return binder.apply(oldSupplier.get()).getLeft(); + }; + + final Supplier<NewRightType> rightSupp = () -> { + if (pairBound) return mapper.apply(boundPair.getRight()); + + final NewRight rightVal = binder.apply(oldSupplier.get()).getRight(); + + return mapper.apply(rightVal); + }; + + return new LazyPair<>(leftSupp, rightSupp); + } + + @Override + public <MergedType> MergedType merge(final BiFunction<NewLeft, NewRight, MergedType> merger) { + if (!pairBound) { + boundPair = binder.apply(oldSupplier.get()); + + pairBound = true; + } + + return boundPair.merge(merger); + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/data/internals/WrappedLazy.java b/base/src/main/java/bjc/utils/data/internals/WrappedLazy.java new file mode 100644 index 0000000..4175724 --- /dev/null +++ b/base/src/main/java/bjc/utils/data/internals/WrappedLazy.java @@ -0,0 +1,62 @@ +package bjc.utils.data.internals; + +import java.util.function.Function; +import java.util.function.UnaryOperator; + +import bjc.utils.data.IHolder; +import bjc.utils.data.Lazy; + +public class WrappedLazy<ContainedType> implements IHolder<ContainedType> { + private final IHolder<IHolder<ContainedType>> held; + + public WrappedLazy(final IHolder<ContainedType> wrappedHolder) { + held = new Lazy<>(wrappedHolder); + } + + // This has an extra parameter, because otherwise it erases to the same + // as the public one + private WrappedLazy(final IHolder<IHolder<ContainedType>> wrappedHolder, final boolean dummy) { + held = wrappedHolder; + } + + @Override + public <BoundType> IHolder<BoundType> bind(final Function<ContainedType, IHolder<BoundType>> binder) { + final IHolder<IHolder<BoundType>> newHolder = held.map((containedHolder) -> { + return containedHolder.bind(binder); + }); + + return new WrappedLazy<>(newHolder, false); + } + + @Override + public <NewType> Function<ContainedType, IHolder<NewType>> lift(final Function<ContainedType, NewType> func) { + return (val) -> { + return new Lazy<>(func.apply(val)); + }; + } + + @Override + public <MappedType> IHolder<MappedType> map(final Function<ContainedType, MappedType> mapper) { + final IHolder<IHolder<MappedType>> newHolder = held.map((containedHolder) -> { + return containedHolder.map(mapper); + }); + + return new WrappedLazy<>(newHolder, false); + } + + @Override + public IHolder<ContainedType> transform(final UnaryOperator<ContainedType> transformer) { + held.transform((containedHolder) -> { + return containedHolder.transform(transformer); + }); + + return this; + } + + @Override + public <UnwrappedType> UnwrappedType unwrap(final Function<ContainedType, UnwrappedType> unwrapper) { + return held.unwrap((containedHolder) -> { + return containedHolder.unwrap(unwrapper); + }); + } +} diff --git a/base/src/main/java/bjc/utils/data/internals/WrappedOption.java b/base/src/main/java/bjc/utils/data/internals/WrappedOption.java new file mode 100644 index 0000000..512c699 --- /dev/null +++ b/base/src/main/java/bjc/utils/data/internals/WrappedOption.java @@ -0,0 +1,76 @@ +package bjc.utils.data.internals; + +import java.util.function.Function; +import java.util.function.UnaryOperator; + +import bjc.utils.data.IHolder; +import bjc.utils.data.Option; + +public class WrappedOption<ContainedType> implements IHolder<ContainedType> { + private final IHolder<IHolder<ContainedType>> held; + + public WrappedOption(final IHolder<ContainedType> seedValue) { + held = new Option<>(seedValue); + } + + private WrappedOption(final IHolder<IHolder<ContainedType>> toHold, final boolean dummy) { + held = toHold; + } + + @Override + public <BoundType> IHolder<BoundType> bind(final Function<ContainedType, IHolder<BoundType>> binder) { + final IHolder<IHolder<BoundType>> newHolder = held.map((containedHolder) -> { + return containedHolder.bind((containedValue) -> { + if (containedValue == null) return new Option<>(null); + + return binder.apply(containedValue); + }); + }); + + return new WrappedOption<>(newHolder, false); + } + + @Override + public <NewType> Function<ContainedType, IHolder<NewType>> lift(final Function<ContainedType, NewType> func) { + return (val) -> { + return new Option<>(func.apply(val)); + }; + } + + @Override + public <MappedType> IHolder<MappedType> map(final Function<ContainedType, MappedType> mapper) { + final IHolder<IHolder<MappedType>> newHolder = held.map((containedHolder) -> { + return containedHolder.map((containedValue) -> { + if (containedValue == null) return null; + + return mapper.apply(containedValue); + }); + }); + + return new WrappedOption<>(newHolder, false); + } + + @Override + public IHolder<ContainedType> transform(final UnaryOperator<ContainedType> transformer) { + held.transform((containedHolder) -> { + return containedHolder.transform((containedValue) -> { + if (containedValue == null) return null; + + return transformer.apply(containedValue); + }); + }); + + return this; + } + + @Override + public <UnwrappedType> UnwrappedType unwrap(final Function<ContainedType, UnwrappedType> unwrapper) { + return held.unwrap((containedHolder) -> { + return containedHolder.unwrap((containedValue) -> { + if (containedValue == null) return null; + + return unwrapper.apply(containedValue); + }); + }); + } +} diff --git a/base/src/main/java/bjc/utils/esodata/AbbrevMap.java b/base/src/main/java/bjc/utils/esodata/AbbrevMap.java new file mode 100644 index 0000000..0d54471 --- /dev/null +++ b/base/src/main/java/bjc/utils/esodata/AbbrevMap.java @@ -0,0 +1,227 @@ +package bjc.utils.esodata; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.SetMultimap; + +import bjc.utils.funcdata.FunctionalMap; +import bjc.utils.funcdata.IMap; + +/** + * Represents a mapping from a set of strings to a mapping of all unambiguous + * prefixes of their respective strings. + * + * This works the same as Ruby's Abbrev. + * + * @author EVE + * + */ +public class AbbrevMap { + /* + * All of the words we have abbreviations for. + */ + private final Set<String> wrds; + + /* + * Maps abbreviations to their strings. + */ + private IMap<String, String> abbrevMap; + + /* + * Counts how many times we've seen a substring. + */ + private Set<String> seen; + + /* + * Maps ambiguous abbreviations to the strings they could be. + */ + private SetMultimap<String, String> ambMap; + + /** + * Create a new abbreviation map. + * + * @param words + * The initial set of words to put in the map. + */ + public AbbrevMap(final String... words) { + wrds = new HashSet<>(Arrays.asList(words)); + + recalculate(); + } + + /** + * Recalculate all the abbreviations in this map. + */ + public void recalculate() { + abbrevMap = new FunctionalMap<>(); + + ambMap = HashMultimap.create(); + + seen = new HashSet<>(); + + for (final String word : wrds) { + /* + * A word always abbreviates to itself. + */ + abbrevMap.put(word, word); + + intAddWord(word); + } + } + + /** + * Adds words to the abbreviation map. + * + * @param words + * The words to add to the abbreviation map. + */ + public void addWords(final String... words) { + wrds.addAll(Arrays.asList(words)); + + for (final String word : words) { + /* + * A word always abbreviates to itself. + */ + abbrevMap.put(word, word); + + intAddWord(word); + } + } + + /* + * Actually add abbreviations of a word. + */ + private void intAddWord(final String word) { + /* + * Skip blank words. + */ + if (word.equals("")) return; + + /* + * Handle each possible abbreviation. + */ + for (int i = word.length(); i > 0; i--) { + final String subword = word.substring(0, i); + + if (seen.contains(subword)) { + /* + * Remove a mapping if its ambiguous and not a + * whole word. + */ + if (abbrevMap.containsKey(subword) && !wrds.contains(subword)) { + final String oldword = abbrevMap.remove(subword); + + ambMap.put(subword, oldword); + ambMap.put(subword, word); + } else if (!wrds.contains(subword)) { + ambMap.put(subword, word); + } + } else { + seen.add(subword); + + abbrevMap.put(subword, word); + } + } + } + + /** + * Removes words from the abbreviation map. + * + * NOTE: There may be inconsistent behavior after removing a word from + * the map. Use {@link AbbrevMap#recalculate()} to fix it if it occurs. + * + * @param words + * The words to remove. + */ + public void removeWords(final String... words) { + wrds.removeAll(Arrays.asList(words)); + + for (final String word : words) { + intRemoveWord(word); + } + } + + /* + * Actually remove a word. + */ + private void intRemoveWord(final String word) { + /* + * Skip blank words. + */ + if (word.equals("")) return; + + /* + * Handle each possible abbreviation. + */ + for (int i = word.length(); i > 0; i--) { + final String subword = word.substring(0, i); + + if (abbrevMap.containsKey(subword)) { + abbrevMap.remove(subword); + } else { + ambMap.remove(subword, word); + + final Set<String> possWords = ambMap.get(subword); + + if (possWords.size() == 0) { + seen.remove(subword); + } else if (possWords.size() == 1) { + final String newWord = possWords.iterator().next(); + + abbrevMap.put(subword, newWord); + ambMap.remove(subword, newWord); + } + } + } + } + + /** + * Convert an abbreviation into all the strings it could abbreviate + * into. + * + * @param abbrev + * The abbreviation to convert. + * + * @return All the expansions for the provided abbreviation. + */ + public String[] deabbrev(final String abbrev) { + if (abbrevMap.containsKey(abbrev)) + return new String[] { abbrevMap.get(abbrev) }; + else return ambMap.get(abbrev).toArray(new String[0]); + } + + @Override + public int hashCode() { + final int prime = 31; + + int result = 1; + result = prime * result + (wrds == null ? 0 : wrds.hashCode()); + + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof AbbrevMap)) return false; + + final AbbrevMap other = (AbbrevMap) obj; + + if (wrds == null) { + if (other.wrds != null) return false; + } else if (!wrds.equals(other.wrds)) return false; + + return true; + } + + @Override + public String toString() { + final String fmt = "AbbrevMap [wrds=%s, abbrevMap=%s, seen=%s, ambMap=%s]"; + + return String.format(fmt, wrds, abbrevMap, seen, ambMap); + } +} diff --git a/base/src/main/java/bjc/utils/esodata/Directory.java b/base/src/main/java/bjc/utils/esodata/Directory.java new file mode 100644 index 0000000..17b70f5 --- /dev/null +++ b/base/src/main/java/bjc/utils/esodata/Directory.java @@ -0,0 +1,106 @@ +package bjc.utils.esodata;
+
+/**
+ * Represents a hierarchical map.
+ *
+ * What's useful about this is that you can hand sub-directories to people and
+ * be able to ensure that they can't write outside of it.
+ *
+ * @param <K>
+ * The key type of the map.
+ * @param <V>
+ * The value type of the map.
+ */
+public interface Directory<K, V> {
+ /**
+ * Retrieves a given sub-directory.
+ *
+ * @param key
+ * The key to retrieve the sub-directory for.
+ *
+ * @return The sub-directory under that name.
+ *
+ * @throws IllegalArgumentException
+ * If the given sub-directory doesn't exist.
+ */
+ Directory<K, V> getSubdirectory(K key);
+
+ /**
+ * Check if a given sub-directory exists.
+ *
+ * @param key
+ * The key to look for the sub-directory under.
+ *
+ * @return Whether or not a sub-directory of that name exists.
+ */
+ boolean hasSubdirectory(K key);
+
+ /**
+ * Insert a sub-directory into the dictionary.
+ *
+ * @param key
+ * The name of the new sub-directory
+ * @param value
+ * The sub-directory to insert
+ *
+ * @return The old sub-directory attached to this key, or null if such a
+ * sub-directory didn't exist
+ */
+ Directory<K, V> putSubdirectory(K key, Directory<K, V> value);
+
+ /**
+ * Create a new sub-directory.
+ *
+ * Will fail if a sub-directory of that name already exists.
+ *
+ * @param key
+ * The name of the new sub-directory.
+ *
+ * @return The new sub-directory, or null if one by that name already
+ * exists.
+ */
+ default Directory<K, V> newSubdirectory(final K key) {
+ if (hasSubdirectory(key)) return null;
+
+ final Directory<K, V> dir = new SimpleDirectory<>();
+
+ putSubdirectory(key, dir);
+
+ return dir;
+ }
+
+ /**
+ * Check if the directory contains a data-item under the given key.
+ *
+ * @param key
+ * The key to check for.
+ *
+ * @return Whether or not there is a data item for the given key.
+ */
+ boolean containsKey(K key);
+
+ /**
+ * Retrieve a given data-item from the directory.
+ *
+ * @param key
+ * The key to retrieve data for.
+ *
+ * @return The value for the given key.
+ *
+ * @throws IllegalArgumentException
+ * If no value exists for the given key.
+ */
+ V getKey(K key);
+
+ /**
+ * Insert a data-item into the directory.
+ *
+ * @param key
+ * The key to insert into.
+ * @param val
+ * The value to insert.
+ *
+ * @return The old value of key, or null if such a value didn't exist.
+ */
+ V putKey(K key, V val);
+}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/esodata/DoubleTape.java b/base/src/main/java/bjc/utils/esodata/DoubleTape.java new file mode 100644 index 0000000..5c463c6 --- /dev/null +++ b/base/src/main/java/bjc/utils/esodata/DoubleTape.java @@ -0,0 +1,258 @@ +package bjc.utils.esodata; + +/** + * Double-sided tape is essentially two tapes stuck together with a shared + * cursor. + * + * The main way a double-sided tape differs is that it can be flipped, allowing + * access to another set of data. + * + * However, there is only one cursor, and the position of the cursor on one side + * is the inverse of the position on the other side. + * + * When one side is extended, a null will be inserted into the inactive side + * regardless of the auto-extension policy of the tape. The policy will still be + * respected for the active side. + * + * All operations that refer to the tape refer to the currently active side of + * the tape, except for flip. + * + * Flip refers to the entire tape for 'obvious' reasons. + * + * @param <T> + * The element type of the tape. + * @author bjculkin + */ +public class DoubleTape<T> implements Tape<T> { + private Tape<T> front; + private Tape<T> back; + + /** + * Create a new empty double-sided tape that doesn't autoextend. + */ + public DoubleTape() { + this(false); + } + + /** + * Create a new empty double-sided tape that follows the specified + * auto-extension policy. + * + * @param autoExtnd + * Whether or not to auto-extend the tape to the right w/ + * nulls. + */ + public DoubleTape(final boolean autoExtnd) { + front = new SingleTape<>(autoExtnd); + back = new SingleTape<>(autoExtnd); + } + + /** + * Get the item the tape is currently on. + * + * @return The item the tape is on. + */ + @Override + public T item() { + return front.item(); + } + + /** + * Set the item the tape is currently on. + * + * @param itm + * The new value for the tape item. + */ + @Override + public void item(final T itm) { + front.item(itm); + } + + /** + * Get the current number of elements in the tape. + * + * @return The current number of elements in the tape. + */ + @Override + public int size() { + return front.size(); + } + + @Override + public int position() { + return front.position(); + } + + /** + * Insert an element before the current item. + * + * @param itm + * The item to add. + */ + @Override + public void insertBefore(final T itm) { + front.insertBefore(itm); + back.insertAfter(null); + } + + /** + * Insert an element after the current item. + */ + @Override + public void insertAfter(final T itm) { + front.insertAfter(itm); + back.insertBefore(itm); + } + + /** + * Remove the current element. + * + * Also moves the cursor back one step if possible to maintain relative + * position, and removes the corresponding item from the non-active side + * + * @return The removed item from the active side. + */ + @Override + public T remove() { + back.remove(); + + return front.remove(); + } + + /** + * Move the cursor to the left-most position. + */ + @Override + public void first() { + front.first(); + back.last(); + } + + /** + * Move the cursor the right-most position. + */ + @Override + public void last() { + front.last(); + back.first(); + } + + /** + * Move the cursor one space left. + * + * The cursor can't go past zero. + * + * @return True if the cursor was moved left. + */ + @Override + public boolean left() { + return left(1); + } + + /** + * Move the cursor the specified amount left. + * + * The cursor can't go past zero. Attempts to move the cursor by amounts + * that would exceed zero don't move the cursor at all. + * + * @param amt + * The amount to attempt to move the cursor left. + * + * @return True if the cursor was moved left. + */ + @Override + public boolean left(final int amt) { + final boolean succ = front.left(amt); + + if (succ) { + back.right(amt); + } + + return succ; + } + + /** + * Move the cursor one space right. + * + * Moving the cursor right will auto-extend the tape if that is enabled. + * + * @return Whether the cursor was moved right. + */ + @Override + public boolean right() { + return right(1); + } + + /** + * Move the cursor the specified amount right. + * + * Moving the cursor right will auto-extend the tape if that is enabled. + * + * @param amt + * The amount to move the cursor right by. + * + * @return Whether the cursor was moved right. + */ + @Override + public boolean right(final int amt) { + final boolean succ = front.right(amt); + + if (succ) { + back.left(amt); + } + + return succ; + } + + /** + * Flips the tape. + * + * The active side becomes inactive, and the inactive side becomes + * active. + */ + public void flip() { + final Tape<T> tmp = front; + + front = back; + + back = tmp; + } + + @Override + public boolean isDoubleSided() { + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (back == null ? 0 : back.hashCode()); + result = prime * result + (front == null ? 0 : front.hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof DoubleTape<?>)) return false; + + final DoubleTape<?> other = (DoubleTape<?>) obj; + + if (back == null) { + if (other.back != null) return false; + } else if (!back.equals(other.back)) return false; + + if (front == null) { + if (other.front != null) return false; + } else if (!front.equals(other.front)) return false; + + return true; + } + + @Override + public String toString() { + return String.format("DoubleTape [front=%s, back=%s]", front, back); + } +} diff --git a/base/src/main/java/bjc/utils/esodata/PushdownMap.java b/base/src/main/java/bjc/utils/esodata/PushdownMap.java new file mode 100644 index 0000000..a631704 --- /dev/null +++ b/base/src/main/java/bjc/utils/esodata/PushdownMap.java @@ -0,0 +1,148 @@ +package bjc.utils.esodata; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +import bjc.utils.funcdata.FunctionalMap; +import bjc.utils.funcdata.IList; +import bjc.utils.funcdata.IMap; + +/** + * A variant of a map where inserting a duplicate key shadows the existing value + * instead of replacing it. + * + * @author EVE + * + * @param <KeyType> + * The key of the map. + * @param <ValueType> + * The values in the map. + */ +public class PushdownMap<KeyType, ValueType> implements IMap<KeyType, ValueType> { + private final IMap<KeyType, Stack<ValueType>> backing; + + /** + * Create a new empty stack-based map. + */ + public PushdownMap() { + backing = new FunctionalMap<>(); + } + + private PushdownMap(final IMap<KeyType, Stack<ValueType>> back) { + backing = back; + } + + @Override + public void clear() { + backing.clear(); + } + + @Override + public boolean containsKey(final KeyType key) { + return backing.containsKey(key); + } + + @Override + public IMap<KeyType, ValueType> extend() { + return new PushdownMap<>(backing.extend()); + } + + @Override + public void forEach(final BiConsumer<KeyType, ValueType> action) { + backing.forEach((key, stk) -> action.accept(key, stk.top())); + } + + @Override + public void forEachKey(final Consumer<KeyType> action) { + backing.forEachKey(action); + } + + @Override + public void forEachValue(final Consumer<ValueType> action) { + backing.forEachValue(stk -> action.accept(stk.top())); + } + + @Override + public ValueType get(final KeyType key) { + return backing.get(key).top(); + } + + @Override + public int size() { + return backing.size(); + } + + @Override + public IList<KeyType> keyList() { + return backing.keyList(); + } + + @Override + public <V2> IMap<KeyType, V2> transform(final Function<ValueType, V2> transformer) { + throw new UnsupportedOperationException("Cannot transform pushdown maps."); + } + + @Override + public ValueType put(final KeyType key, final ValueType val) { + if (backing.containsKey(key)) { + final Stack<ValueType> stk = backing.get(key); + + final ValueType vl = stk.top(); + + stk.push(val); + + return vl; + } else { + final Stack<ValueType> stk = new SimpleStack<>(); + + stk.push(val); + + return null; + } + } + + @Override + public ValueType remove(final KeyType key) { + final Stack<ValueType> stk = backing.get(key); + + if (stk.size() > 1) + return stk.pop(); + else return backing.remove(key).top(); + } + + @Override + public IList<ValueType> valueList() { + return backing.valueList().map(stk -> stk.top()); + } + + @Override + public int hashCode() { + final int prime = 31; + + int result = 1; + result = prime * result + (backing == null ? 0 : backing.hashCode()); + + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof PushdownMap<?, ?>)) return false; + + final PushdownMap<?, ?> other = (PushdownMap<?, ?>) obj; + + if (backing == null) { + if (other.backing != null) return false; + } else if (!backing.equals(other.backing)) return false; + + return true; + } + + @Override + public String toString() { + return String.format("PushdownMap [backing=%s]", backing); + } +} diff --git a/base/src/main/java/bjc/utils/esodata/QueueStack.java b/base/src/main/java/bjc/utils/esodata/QueueStack.java new file mode 100644 index 0000000..850598a --- /dev/null +++ b/base/src/main/java/bjc/utils/esodata/QueueStack.java @@ -0,0 +1,88 @@ +package bjc.utils.esodata; + +import java.util.Deque; +import java.util.LinkedList; + +/** + * A FIFO implementation of a stack. + * + * @param <T> + * The datatype stored in the stack. + * @author Ben Culkin + */ +public class QueueStack<T> extends Stack<T> { + private final Deque<T> backing; + + /** + * Create a new empty stack queue. + * + */ + public QueueStack() { + backing = new LinkedList<>(); + } + + @Override + public void push(final T elm) { + backing.add(elm); + } + + @Override + public T pop() { + if (backing.isEmpty()) throw new StackUnderflowException(); + + return backing.remove(); + } + + @Override + public T top() { + if (backing.isEmpty()) throw new StackUnderflowException(); + + return backing.peek(); + } + + @Override + public int size() { + return backing.size(); + } + + @Override + public boolean empty() { + return backing.size() == 0; + } + + @SuppressWarnings("unchecked") + @Override + public T[] toArray() { + return (T[]) backing.toArray(); + } + + @Override + public String toString() { + return String.format("QueueStack [backing=%s]", backing); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + + result = prime * result + (backing == null ? 0 : backing.hashCode()); + + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof QueueStack<?>)) return false; + + final QueueStack<?> other = (QueueStack<?>) obj; + + if (backing == null) { + if (other.backing != null) return false; + } else if (!backing.equals(other.backing)) return false; + + return true; + } +} diff --git a/base/src/main/java/bjc/utils/esodata/SimpleDirectory.java b/base/src/main/java/bjc/utils/esodata/SimpleDirectory.java new file mode 100644 index 0000000..69fd019 --- /dev/null +++ b/base/src/main/java/bjc/utils/esodata/SimpleDirectory.java @@ -0,0 +1,95 @@ +package bjc.utils.esodata; + +import bjc.utils.funcdata.FunctionalMap; +import bjc.utils.funcdata.IMap; + +/** + * Simple implementation of {@link Directory}. + * + * Has a split namespace for data and children. + * + * @author EVE + * + * @param <K> + * The key type of the directory. + * @param <V> + * The value type of the directory. + */ +public class SimpleDirectory<K, V> implements Directory<K, V> { + private final IMap<K, Directory<K, V>> children; + + private final IMap<K, V> data; + + /** + * Create a new directory. + */ + public SimpleDirectory() { + children = new FunctionalMap<>(); + data = new FunctionalMap<>(); + } + + @Override + public Directory<K, V> getSubdirectory(final K key) { + return children.get(key); + } + + @Override + public boolean hasSubdirectory(final K key) { + return children.containsKey(key); + } + + @Override + public Directory<K, V> putSubdirectory(final K key, final Directory<K, V> val) { + return children.put(key, val); + } + + @Override + public boolean containsKey(final K key) { + return data.containsKey(key); + } + + @Override + public V getKey(final K key) { + return data.get(key); + } + + @Override + public V putKey(final K key, final V val) { + return data.put(key, val); + } + + @Override + public int hashCode() { + final int prime = 31; + + int result = 1; + result = prime * result + (children == null ? 0 : children.hashCode()); + result = prime * result + (data == null ? 0 : data.hashCode()); + + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof SimpleDirectory<?, ?>)) return false; + + final SimpleDirectory<?, ?> other = (SimpleDirectory<?, ?>) obj; + + if (children == null) { + if (other.children != null) return false; + } else if (!children.equals(other.children)) return false; + + if (data == null) { + if (other.data != null) return false; + } else if (!data.equals(other.data)) return false; + + return true; + } + + @Override + public String toString() { + return String.format("SimpleDirectory [children=%s, data=%s]", children, data); + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/esodata/SimpleStack.java b/base/src/main/java/bjc/utils/esodata/SimpleStack.java new file mode 100644 index 0000000..fdb3300 --- /dev/null +++ b/base/src/main/java/bjc/utils/esodata/SimpleStack.java @@ -0,0 +1,88 @@ +package bjc.utils.esodata; + +import java.util.Deque; +import java.util.LinkedList; + +/** + * Simple implementation of a stack. + * + * @param <T> + * The datatype stored in the stack. + * @author Ben Culkin + */ +public class SimpleStack<T> extends Stack<T> { + private final Deque<T> backing; + + /** + * Create a new empty stack. + * + */ + public SimpleStack() { + backing = new LinkedList<>(); + } + + @Override + public void push(final T elm) { + backing.push(elm); + } + + @Override + public T pop() { + if (backing.isEmpty()) throw new StackUnderflowException(); + + return backing.pop(); + } + + @Override + public T top() { + if (backing.isEmpty()) throw new StackUnderflowException(); + + return backing.peek(); + } + + @Override + public int size() { + return backing.size(); + } + + @Override + public boolean empty() { + return backing.size() == 0; + } + + @Override + @SuppressWarnings("unchecked") + public T[] toArray() { + return (T[]) backing.toArray(); + } + + @Override + public int hashCode() { + final int prime = 31; + + int result = 1; + result = prime * result + (backing == null ? 0 : backing.hashCode()); + + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof SimpleStack<?>)) return false; + + final SimpleStack<?> other = (SimpleStack<?>) obj; + + if (backing == null) { + if (other.backing != null) return false; + } else if (!backing.equals(other.backing)) return false; + + return true; + } + + @Override + public String toString() { + return String.format("SimpleStack [backing=%s]", backing); + } +} diff --git a/base/src/main/java/bjc/utils/esodata/SingleTape.java b/base/src/main/java/bjc/utils/esodata/SingleTape.java new file mode 100644 index 0000000..c50be92 --- /dev/null +++ b/base/src/main/java/bjc/utils/esodata/SingleTape.java @@ -0,0 +1,255 @@ +package bjc.utils.esodata; + +import java.util.ArrayList; + +/** + * A tape is a one-dimensional array that can only be accessed in one position + * at a time. + * + * A tape is essentially a 1D array with a cursor attached to it, and you can + * only affect elements at that cursor. The size of the array is theoretically + * unbounded to the right, but in practice bounded by available memory. + * + * You can choose whether or not you want the tape to automatically extend + * itself to the right with null elements by specifying its auto-extension + * policy. + * + * @param <T> + * The element type of the tape. + * + * @author bjculkin + */ +public class SingleTape<T> implements Tape<T> { + protected ArrayList<T> backing; + protected int pos; + + protected boolean autoExtend; + + /** + * Create a new tape with the specified contents that doesn't + * autoextend. + */ + public SingleTape(T... vals) { + autoExtend = false; + + backing = new ArrayList(vals.length); + + for(T val : vals) { + backing.add(val); + } + } + /** + * Create a new empty tape that doesn't autoextend. + */ + public SingleTape() { + this(false); + } + + /** + * Create a new empty tape that follows the specified auto-extension + * policy. + * + * @param autoExtnd + * Whether or not to auto-extend the tape to the right w/ + * nulls. + */ + public SingleTape(final boolean autoExtnd) { + autoExtend = autoExtnd; + + backing = new ArrayList<>(); + } + + /** + * Get the item the tape is currently on. + * + * @return The item the tape is on. + */ + @Override + public T item() { + return backing.get(pos); + } + + /** + * Set the item the tape is currently on. + * + * @param itm + * The new value for the tape item. + */ + @Override + public void item(final T itm) { + backing.set(pos, itm); + } + + /** + * Get the current number of elements in the tape. + * + * @return The current number of elements in the tape. + */ + @Override + public int size() { + return backing.size(); + } + + @Override + public int position() { + return pos; + } + + /** + * Insert an element before the current item. + * + * @param itm + * The item to add. + */ + @Override + public void insertBefore(final T itm) { + backing.add(pos, itm); + } + + /** + * Insert an element after the current item. + */ + @Override + public void insertAfter(final T itm) { + if (pos == backing.size() - 1) { + backing.add(itm); + } else { + backing.add(pos + 1, itm); + } + } + + /** + * Remove the current element. + * + * Also moves the cursor back one step if possible to maintain relative + * position. + * + * @return The removed item. + */ + @Override + public T remove() { + final T res = backing.remove(pos); + if (pos != 0) { + pos -= 1; + } + return res; + } + + /** + * Move the cursor to the left-most position. + */ + @Override + public void first() { + pos = 0; + } + + /** + * Move the cursor the right-most position. + */ + @Override + public void last() { + pos = backing.size() - 1; + } + + /** + * Move the cursor one space left. + * + * The cursor can't go past zero. + * + * @return True if the cursor was moved left. + */ + @Override + public boolean left() { + return left(1); + } + + /** + * Move the cursor the specified amount left. + * + * The cursor can't go past zero. Attempts to move the cursor by amounts + * that would exceed zero don't move the cursor at all. + * + * @param amt + * The amount to attempt to move the cursor left. + * + * @return True if the cursor was moved left. + */ + @Override + public boolean left(final int amt) { + if (pos - amt < 0) return false; + + pos -= amt; + return true; + } + + /** + * Move the cursor one space right. + * + * Moving the cursor right will auto-extend the tape if that is enabled. + * + * @return Whether the cursor was moved right. + */ + @Override + public boolean right() { + return right(1); + } + + /** + * Move the cursor the specified amount right. + * + * Moving the cursor right will auto-extend the tape if that is enabled. + * + * @param amt + * The amount to move the cursor right by. + * + * @return Whether the cursor was moved right. + */ + @Override + public boolean right(final int amt) { + if (pos + amt >= backing.size() - 1) { + if (autoExtend) { + while (pos + amt >= backing.size() - 1) { + backing.add(null); + } + } else return false; + } + + pos += amt; + return true; + } + + @Override + public boolean isDoubleSided() { + return false; + } + + @Override + public int hashCode() { + final int prime = 31; + + int result = 1; + result = prime * result + (backing == null ? 0 : backing.hashCode()); + + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof SingleTape<?>)) return false; + + final SingleTape<?> other = (SingleTape<?>) obj; + + if (backing == null) { + if (other.backing != null) return false; + } else if (!backing.equals(other.backing)) return false; + + return true; + } + + @Override + public String toString() { + return String.format("SingleTape [backing=%s, pos=%s, autoExtend=%s]", backing, pos, autoExtend); + } +} diff --git a/base/src/main/java/bjc/utils/esodata/SpaghettiStack.java b/base/src/main/java/bjc/utils/esodata/SpaghettiStack.java new file mode 100644 index 0000000..7c8c757 --- /dev/null +++ b/base/src/main/java/bjc/utils/esodata/SpaghettiStack.java @@ -0,0 +1,99 @@ +package bjc.utils.esodata; + +import java.util.Arrays; +import java.util.stream.Stream; + +/* + * Implements a spaghetti stack, which is a stack that is branched off of a + * parent stack. + * + * @param T The datatype stored in the stack. + * @author Ben Culkin + */ +class SpaghettiStack<T> extends Stack<T> { + private final Stack<T> backing; + + private final Stack<T> parent; + + /** + * Create a new empty spaghetti stack, off of the specified parent. + * + * @param par + * The parent stack + */ + public SpaghettiStack(final Stack<T> par) { + backing = new SimpleStack<>(); + + parent = par; + } + + @Override + public void push(final T elm) { + backing.push(elm); + } + + @Override + public T pop() { + if (backing.empty()) return parent.pop(); + + return backing.pop(); + } + + @Override + public T top() { + if (backing.empty()) return parent.top(); + + return backing.top(); + } + + @Override + public int size() { + return parent.size() + backing.size(); + } + + @Override + public boolean empty() { + return backing.empty() && parent.empty(); + } + + @SuppressWarnings("unchecked") + @Override + public T[] toArray() { + return (T[]) Stream.concat(Arrays.stream(parent.toArray()), Arrays.stream(backing.toArray())).toArray(); + } + + @Override + public int hashCode() { + final int prime = 31; + + int result = 1; + result = prime * result + (backing == null ? 0 : backing.hashCode()); + result = prime * result + (parent == null ? 0 : parent.hashCode()); + + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof SpaghettiStack<?>)) return false; + + final SpaghettiStack<?> other = (SpaghettiStack<?>) obj; + + if (backing == null) { + if (other.backing != null) return false; + } else if (!backing.equals(other.backing)) return false; + + if (parent == null) { + if (other.parent != null) return false; + } else if (!parent.equals(other.parent)) return false; + + return true; + } + + @Override + public String toString() { + return String.format("SpaghettiStack [backing=%s, parent=%s]", backing, parent); + } +} diff --git a/base/src/main/java/bjc/utils/esodata/Stack.java b/base/src/main/java/bjc/utils/esodata/Stack.java new file mode 100644 index 0000000..9d74e9a --- /dev/null +++ b/base/src/main/java/bjc/utils/esodata/Stack.java @@ -0,0 +1,459 @@ +package bjc.utils.esodata; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * A stack, with support for combinators. + * + * A FILO stack with support for forth/factor style combinators. + * + * <p> + * <h2>Stack underflow</h2> + * <p> + * NOTE: In general, using any operation that attempts to remove more data from + * the stack than exists will cause a {@link StackUnderflowException} to be + * thrown. Check the size of the stack if you want to avoid this. + * <p> + * </p> + * + * @param <T> + * The datatype stored in the stack. + * + * @author Ben Culkin + */ +public abstract class Stack<T> { + /** + * The exception thrown when attempting to access an element from the + * stack that isn't there. + * + * @author EVE + * + */ + public static class StackUnderflowException extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = 1423867176204571539L; + } + + /** + * Push an element onto the stack. + * + * @param elm + * The element to insert. + */ + public abstract void push(T elm); + + /** + * Pop an element off of the stack. + * + * @return The element on top of the stack. + */ + public abstract T pop(); + + /** + * Retrieve the top element of this stack without removing it from the + * stack. + * + * @return The top element of this stack. + */ + public abstract T top(); + + /** + * Get the number of elements in the stack. + * + * @return the number of elements in the stack. + */ + public abstract int size(); + + /** + * Check if the stack is empty. + * + * @return Whether or not the stack is empty. + */ + public abstract boolean empty(); + + /** + * Create a spaghetti stack branching off of this one. + * + * @return A spaghetti stack with this stack as a parent. + */ + public Stack<T> spaghettify() { + return new SpaghettiStack<>(this); + } + + /* + * Basic combinators + */ + + /** + * Drop n items from the stack. + * + * @param n + * The number of items to drop. + */ + public void drop(final int n) { + for (int i = 0; i < n; i++) { + pop(); + } + } + + /** + * Drop one item from the stack. + */ + public void drop() { + drop(1); + } + + /** + * Delete n items below the current one. + * + * @param n + * The number of items below the top to delete. + */ + public void nip(final int n) { + final T elm = pop(); + + drop(n); + + push(elm); + } + + /** + * Delete the second element in the stack. + */ + public void nip() { + nip(1); + } + + /** + * Replicate the top n items of the stack m times. + * + * @param n + * The number of items to duplicate. + * @param m + * The number of times to duplicate items. + */ + public void multidup(final int n, final int m) { + final List<T> lst = new ArrayList<>(n); + + for (int i = n; i > 0; i--) { + lst.set(i - 1, pop()); + } + + for (int i = 0; i < m; i++) { + for (final T elm : lst) { + push(elm); + } + } + } + + /** + * Duplicate the top n items of the stack. + * + * @param n + * The number of items to duplicate. + */ + public void dup(final int n) { + multidup(n, 2); + } + + /** + * Duplicate the top item on the stack. + */ + public void dup() { + dup(1); + } + + /** + * Replicate the n elements below the top one m times. + * + * @param n + * The number of items to duplicate. + * @param m + * The number of times to duplicate items. + */ + public void multiover(final int n, final int m) { + final List<T> lst = new ArrayList<>(n); + + final T elm = pop(); + + for (int i = n; i > 0; i--) { + lst.set(i - 1, pop()); + } + + for (final T nelm : lst) { + push(nelm); + } + push(elm); + + for (int i = 1; i < m; i++) { + for (final T nelm : lst) { + push(nelm); + } + } + } + + /** + * Duplicate the n elements below the top one. + * + * @param n + * The number of items to duplicate. + */ + public void over(final int n) { + multiover(n, 2); + } + + /** + * Duplicate the second item in the stack. + */ + public void over() { + over(1); + } + + /** + * Duplicate the third item in the stack. + */ + public void pick() { + final T z = pop(); + final T y = pop(); + final T x = pop(); + + push(x); + push(y); + push(z); + push(x); + } + + /** + * Swap the top two items on the stack. + */ + public void swap() { + final T y = pop(); + final T x = pop(); + + push(y); + push(x); + } + + /** + * Duplicate the second item below the first item. + */ + public void deepdup() { + final T y = pop(); + final T x = pop(); + + push(x); + push(x); + push(y); + } + + /** + * Swap the second and third items in the stack. + */ + public void deepswap() { + final T z = pop(); + final T y = pop(); + final T x = pop(); + + push(y); + push(x); + push(z); + } + + /** + * Rotate the top three items on the stack + */ + public void rot() { + final T z = pop(); + final T y = pop(); + final T x = pop(); + + push(y); + push(z); + push(x); + } + + /** + * Inversely rotate the top three items on the stack + */ + public void invrot() { + final T z = pop(); + final T y = pop(); + final T x = pop(); + + push(z); + push(x); + push(y); + } + + /* + * Dataflow Combinators + */ + /** + * Hides the top n elements on the stack from cons. + * + * @param n + * The number of elements to hide. + * @param cons + * The action to hide the elements from + */ + public void dip(final int n, final Consumer<Stack<T>> cons) { + final List<T> elms = new ArrayList<>(n); + + for (int i = n; i > 0; i--) { + elms.set(i - 1, pop()); + } + + cons.accept(this); + + for (final T elm : elms) { + push(elm); + } + } + + /** + * Hide the top element of the stack from cons. + * + * @param cons + * The action to hide the top from + */ + public void dip(final Consumer<Stack<T>> cons) { + dip(1, cons); + } + + /** + * Copy the top n elements on the stack, replacing them once cons is + * done. + * + * @param n + * The number of elements to copy. + * @param cons + * The action to execute. + */ + public void keep(final int n, final Consumer<Stack<T>> cons) { + dup(n); + dip(n, cons); + } + + /** + * Apply all the actions in conses to the top n elements of the stack. + * + * @param n + * The number of elements to give to cons. + * @param conses + * The actions to execute. + */ + public void multicleave(final int n, final List<Consumer<Stack<T>>> conses) { + final List<T> elms = new ArrayList<>(n); + + for (int i = n; i > 0; i--) { + elms.set(i - 1, pop()); + } + + for (final Consumer<Stack<T>> cons : conses) { + for (final T elm : elms) { + push(elm); + } + + cons.accept(this); + } + } + + /** + * Apply all the actions in conses to the top element of the stack. + * + * @param conses + * The actions to execute. + */ + public void cleave(final List<Consumer<Stack<T>>> conses) { + multicleave(1, conses); + } + + /** + * Apply every action in cons to n arguments. + * + * @param n + * The number of parameters each action takes. + * @param conses + * The actions to execute. + */ + public void multispread(final int n, final List<Consumer<Stack<T>>> conses) { + final List<List<T>> nelms = new ArrayList<>(conses.size()); + + for (int i = conses.size(); i > 0; i--) { + final List<T> elms = new ArrayList<>(n); + + for (int j = n; j > 0; j--) { + elms.set(j, pop()); + } + + nelms.set(i, elms); + } + + int i = 0; + for (final List<T> elms : nelms) { + for (final T elm : elms) { + push(elm); + } + + conses.get(i).accept(this); + i += 1; + } + } + + /** + * Apply the actions in cons to corresponding elements from the stack. + * + * @param conses + * The actions to execute. + */ + public void spread(final List<Consumer<Stack<T>>> conses) { + multispread(1, conses); + } + + /** + * Apply the action in cons to the first m groups of n arguments. + * + * @param n + * The number of arguments cons takes. + * @param m + * The number of time to call cons. + * @param cons + * The action to execute. + */ + public void multiapply(final int n, final int m, final Consumer<Stack<T>> cons) { + final List<Consumer<Stack<T>>> conses = new ArrayList<>(m); + + for (int i = 0; i < m; i++) { + conses.add(cons); + } + + multispread(n, conses); + } + + /** + * Apply cons n times to the corresponding elements in the stack. + * + * @param n + * The number of times to execute cons. + * @param cons + * The action to execute. + */ + public void apply(final int n, final Consumer<Stack<T>> cons) { + multiapply(1, n, cons); + } + + /* + * Misc. functions + */ + /** + * Get an array representing this stack. + * + * @return The stack as an array. + */ + public abstract T[] toArray(); +} diff --git a/base/src/main/java/bjc/utils/esodata/Tape.java b/base/src/main/java/bjc/utils/esodata/Tape.java new file mode 100644 index 0000000..b6a2c01 --- /dev/null +++ b/base/src/main/java/bjc/utils/esodata/Tape.java @@ -0,0 +1,126 @@ +package bjc.utils.esodata; + +/** + * Interface for something that acts like a tape. + * + * A tape is essentially a 1D array with a cursor attached to it, and you can + * only affect elements at that cursor. The size of the array is theoretically + * unbounded to the right, but in practice bounded by available memory. + * + * @param <T> + * The element type of the tape. + * + * @author bjculkin + */ +public interface Tape<T> { + /** + * Get the item the tape is currently on. + * + * @return The item the tape is on. + */ + T item(); + + /** + * Set the item the tape is currently on. + * + * @param itm + * The new value for the tape item. + */ + void item(T itm); + + /** + * Get the current number of elements in the tape. + * + * @return The current number of elements in the tape. + */ + int size(); + + /** + * Get the position of the current item. + * + * @return The position of the current item. + */ + int position(); + + /** + * Insert an element before the current item. + * + * @param itm + * The item to add. + */ + void insertBefore(T itm); + + /** + * Insert an element after the current item. + * + * @param itm + * The item to insert. + */ + void insertAfter(T itm); + + /** + * Remove the current element. + * + * Also moves the cursor back one step if possible to maintain relative + * position. + * + * @return The removed item. + */ + T remove(); + + /** + * Move the cursor to the left-most position. + */ + void first(); + + /** + * Move the cursor the right-most position. + */ + void last(); + + /** + * Move the cursor one space left. + * + * The cursor can't go past zero. + * + * @return True if the cursor was moved left. + */ + boolean left(); + + /** + * Move the cursor the specified amount left. + * + * The cursor can't go past zero. Attempts to move the cursor by amounts + * that would exceed zero don't move the cursor at all. + * + * @param amt + * The amount to attempt to move the cursor left. + * + * @return True if the cursor was moved left. + */ + boolean left(int amt); + + /** + * Move the cursor one space right. + * + * @return Whether the cursor was moved right. + */ + boolean right(); + + /** + * Move the cursor the specified amount right. + * + * @param amt + * The amount to move the cursor right by. + * + * @return Whether the cursor was moved right. + */ + boolean right(int amt); + + /** + * Is this tape double sided? + * + * @return Whether or not this tape is double-sided. + */ + boolean isDoubleSided(); +} diff --git a/base/src/main/java/bjc/utils/esodata/TapeChanger.java b/base/src/main/java/bjc/utils/esodata/TapeChanger.java new file mode 100644 index 0000000..dc885bc --- /dev/null +++ b/base/src/main/java/bjc/utils/esodata/TapeChanger.java @@ -0,0 +1,363 @@ +package bjc.utils.esodata; + +/** + * A tape changer is essentially a tape of tapes. + * + * It has a current tape that you can do operations to, but also operations to + * add/remove other tapes. + * + * If there is no tape currently loaded into the changer, all the methods will + * either return null/false. + * + * @param <T> + * The element type of the tapes. + */ +public class TapeChanger<T> implements Tape<T> { + private Tape<Tape<T>> tapes; + private Tape<T> currentTape; + + /** + * Create a new empty tape changer. + */ + public TapeChanger() { + tapes = new SingleTape<>(); + } + + /** + * Create a new tape changer with the specified tapes. + * + * @param current + * The tape to mount first. + * @param others + * The tapes to put in this tape changer. + */ + @SafeVarargs + public TapeChanger(final Tape<T> current, final Tape<T>... others) { + this(); + + tapes.insertBefore(current); + + for (final Tape<T> tp : others) { + tapes.insertAfter(tp); + tapes.right(); + } + + tapes.first(); + currentTape = tapes.item(); + } + + /** + * Get the item the tape is currently on. + * + * @return The item the tape is on. + */ + @Override + public T item() { + if (currentTape == null) return null; + + return currentTape.item(); + } + + /** + * Set the item the tape is currently on. + * + * @param itm + * The new value for the tape item. + */ + @Override + public void item(final T itm) { + if (currentTape == null) return; + + currentTape.item(itm); + } + + /** + * Get the current number of elements in the tape. + * + * @return The current number of elements in the tape. + */ + @Override + public int size() { + if (currentTape == null) return 0; + + return currentTape.size(); + } + + @Override + public int position() { + if (currentTape == null) return 0; + + return currentTape.position(); + } + + /** + * Insert an element before the current item. + * + * @param itm + * The item to add. + */ + @Override + public void insertBefore(final T itm) { + if (currentTape == null) return; + + currentTape.insertBefore(itm); + } + + /** + * Insert an element after the current item. + */ + @Override + public void insertAfter(final T itm) { + if (currentTape == null) return; + + currentTape.insertAfter(itm); + } + + /** + * Remove the current element. + * + * Also moves the cursor back one step if possible to maintain relative + * position, and removes the corresponding item from the non-active side + * + * @return The removed item from the active side. + */ + @Override + public T remove() { + if (currentTape == null) return null; + + return currentTape.remove(); + } + + /** + * Move the cursor to the left-most position. + */ + @Override + public void first() { + if (currentTape == null) return; + + currentTape.first(); + } + + /** + * Move the cursor the right-most position. + */ + @Override + public void last() { + if (currentTape == null) return; + + currentTape.last(); + } + + /** + * Move the cursor one space left. + * + * The cursor can't go past zero. + * + * @return True if the cursor was moved left. + */ + @Override + public boolean left() { + return left(1); + } + + /** + * Move the cursor the specified amount left. + * + * The cursor can't go past zero. Attempts to move the cursor by amounts + * that would exceed zero don't move the cursor at all. + * + * @param amt + * The amount to attempt to move the cursor left. + * + * @return True if the cursor was moved left. + */ + @Override + public boolean left(final int amt) { + if (currentTape == null) return false; + + return currentTape.left(amt); + } + + /** + * Move the cursor one space right. + * + * Moving the cursor right will auto-extend the tape if that is enabled. + * + * @return Whether the cursor was moved right. + */ + @Override + public boolean right() { + return right(1); + } + + /** + * Move the cursor the specified amount right. + * + * Moving the cursor right will auto-extend the tape if that is enabled. + * + * @param amt + * The amount to move the cursor right by. + * + * @return Whether the cursor was moved right. + */ + @Override + public boolean right(final int amt) { + if (currentTape == null) return false; + + return currentTape.right(amt); + } + + /** + * Flips the tape. + * + * The active side becomes inactive, and the inactive side becomes + * active. + * + * If the current tape is not double-sided, does nothing. + */ + public void flip() { + if (currentTape == null) return; + + if (currentTape.isDoubleSided()) { + ((DoubleTape<T>) currentTape).flip(); + } + } + + @Override + public boolean isDoubleSided() { + if (currentTape == null) return false; + + return currentTape.isDoubleSided(); + } + + /** + * Check if a tape is currently loaded. + * + * @return Whether or not a tape is loaded. + */ + public boolean isLoaded() { + return currentTape != null; + } + + /** + * Move to the next tape in the changer. + * + * Attempting to load a tape that isn't there won't eject the current + * tape. + * + * @return Whether or not the next tape was loaded. + */ + public boolean nextTape() { + final boolean succ = tapes.right(); + + if (succ) { + currentTape = tapes.item(); + } + + return succ; + } + + /** + * Move to the previous tape in the changer. + * + * Attempting to load a tape that isn't there won't eject the current + * tape. + * + * @return Whether or not the previous tape was loaded. + */ + public boolean prevTape() { + final boolean succ = tapes.left(); + + if (succ) { + currentTape = tapes.item(); + } + + return succ; + } + + /** + * Inserts a tape into the tape changer. + * + * Any currently loaded tape is ejected, and becomes the previous tape. + * + * The specified tape is loaded. + * + * @param tp + * The tape to insert and load. + */ + public void insertTape(final Tape<T> tp) { + tapes.insertAfter(tp); + tapes.right(); + + currentTape = tapes.item(); + } + + /** + * Removes the current tape. + * + * Does nothing if there is not a tape loaded. + * + * Loads the previous tape, if there is one. + * + * @return The removed tape. + */ + public Tape<T> removeTape() { + if (currentTape == null) return null; + + final Tape<T> tp = tapes.remove(); + currentTape = tapes.item(); + + return tp; + } + + /** + * Ejects the current tape. + * + * Does nothing if no tape is loaded. + */ + public void eject() { + currentTape = null; + } + + /** + * Get how many tapes are currently in the changer. + * + * @return How many tapes are currently in the changer. + */ + public int tapeCount() { + return tapes.size(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (currentTape == null ? 0 : currentTape.hashCode()); + result = prime * result + (tapes == null ? 0 : tapes.hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof TapeChanger<?>)) return false; + + final TapeChanger<?> other = (TapeChanger<?>) obj; + + if (currentTape == null) { + if (other.currentTape != null) return false; + } else if (!currentTape.equals(other.currentTape)) return false; + + if (tapes == null) { + if (other.tapes != null) return false; + } else if (!tapes.equals(other.tapes)) return false; + + return true; + } + + @Override + public String toString() { + return String.format("TapeChanger [tapes=%s, currentTape='%s']", tapes, currentTape); + } +} diff --git a/base/src/main/java/bjc/utils/esodata/TapeLibrary.java b/base/src/main/java/bjc/utils/esodata/TapeLibrary.java new file mode 100644 index 0000000..2dbc70b --- /dev/null +++ b/base/src/main/java/bjc/utils/esodata/TapeLibrary.java @@ -0,0 +1,340 @@ +package bjc.utils.esodata; + +import java.util.HashMap; +import java.util.Map; + +/** + * A tape changer is essentially a map of tapes. + * + * It has a current tape that you can do operations to, but also operations to + * add/remove other tapes. + * + * If there is no tape currently loaded into the changer, all the methods will + * either return null/false. + * + * @param <T> + * The element type of the tapes. + */ +public class TapeLibrary<T> implements Tape<T> { + private final Map<String, Tape<T>> tapes; + private Tape<T> currentTape; + + /** + * Create a new empty tape library. + */ + public TapeLibrary() { + tapes = new HashMap<>(); + } + + /** + * Get the item the tape is currently on. + * + * @return The item the tape is on. + */ + @Override + public T item() { + if (currentTape == null) return null; + + return currentTape.item(); + } + + /** + * Set the item the tape is currently on. + * + * @param itm + * The new value for the tape item. + */ + @Override + public void item(final T itm) { + if (currentTape == null) return; + + currentTape.item(itm); + } + + /** + * Get the current number of elements in the tape. + * + * @return The current number of elements in the tape. + */ + @Override + public int size() { + if (currentTape == null) return 0; + + return currentTape.size(); + } + + @Override + public int position() { + if (currentTape == null) return 0; + + return currentTape.position(); + } + /** + * Insert an element before the current item. + * + * @param itm + * The item to add. + */ + @Override + public void insertBefore(final T itm) { + if (currentTape == null) return; + + currentTape.insertBefore(itm); + } + + /** + * Insert an element after the current item. + */ + @Override + public void insertAfter(final T itm) { + if (currentTape == null) return; + + currentTape.insertAfter(itm); + } + + /** + * Remove the current element. + * + * Also moves the cursor back one step if possible to maintain relative + * position, and removes the corresponding item from the non-active side + * + * @return The removed item from the active side. + */ + @Override + public T remove() { + if (currentTape == null) return null; + + return currentTape.remove(); + } + + /** + * Move the cursor to the left-most position. + */ + @Override + public void first() { + if (currentTape == null) return; + + currentTape.first(); + } + + /** + * Move the cursor the right-most position. + */ + @Override + public void last() { + if (currentTape == null) return; + + currentTape.last(); + } + + /** + * Move the cursor one space left. + * + * The cursor can't go past zero. + * + * @return True if the cursor was moved left. + */ + @Override + public boolean left() { + return left(1); + } + + /** + * Move the cursor the specified amount left. + * + * The cursor can't go past zero. Attempts to move the cursor by amounts + * that would exceed zero don't move the cursor at all. + * + * @param amt + * The amount to attempt to move the cursor left. + * + * @return True if the cursor was moved left. + */ + @Override + public boolean left(final int amt) { + if (currentTape == null) return false; + + return currentTape.left(amt); + } + + /** + * Move the cursor one space right. + * + * Moving the cursor right will auto-extend the tape if that is enabled. + * + * @return Whether the cursor was moved right. + */ + @Override + public boolean right() { + return right(1); + } + + /** + * Move the cursor the specified amount right. + * + * Moving the cursor right will auto-extend the tape if that is enabled. + * + * @param amt + * The amount to move the cursor right by. + * + * @return Whether the cursor was moved right. + */ + @Override + public boolean right(final int amt) { + if (currentTape == null) return false; + + return currentTape.right(amt); + } + + /** + * Flips the tape. + * + * The active side becomes inactive, and the inactive side becomes + * active. + * + * If the current tape is not double-sided, does nothing. + */ + public void flip() { + if (currentTape == null) return; + + if (currentTape.isDoubleSided()) { + ((DoubleTape<T>) currentTape).flip(); + } + } + + @Override + public boolean isDoubleSided() { + if (currentTape == null) return false; + + return currentTape.isDoubleSided(); + } + + /** + * Check if a tape is currently loaded. + * + * @return Whether or not a tape is loaded. + */ + public boolean isLoaded() { + return currentTape != null; + } + + /** + * Move to the specified tape in the library. + * + * Attempting to load a tape that isn't there won't eject the current + * tape. + * + * @param label + * The label of the tape to load. + * + * @return Whether or not the next tape was loaded. + */ + public boolean switchTape(final String label) { + if (tapes.containsKey(label)) { + currentTape = tapes.get(label); + return true; + } + + return false; + } + + /** + * Inserts a tape into the tape library. + * + * Any currently loaded tape is ejected. + * + * The specified tape is loaded. + * + * Adding a duplicate tape will overwrite any existing types. + * + * @param label + * The label of the tape to add. + * + * @param tp + * The tape to insert and load. + */ + public void insertTape(final String label, final Tape<T> tp) { + tapes.put(label, tp); + + currentTape = tp; + } + + /** + * Remove a tape from the library. + * + * Does nothing if there is not a tape of that name loaded. + * + * @param label + * The tape to remove. + * + * @return The removed tape. + */ + public Tape<T> removeTape(final String label) { + return tapes.remove(label); + } + + /** + * Ejects the current tape. + * + * Does nothing if no tape is loaded. + */ + public void eject() { + currentTape = null; + } + + /** + * Get how many tapes are currently in the library. + * + * @return How many tapes are currently in the library. + */ + public int tapeCount() { + return tapes.size(); + } + + /** + * Check if a specific tape is loaded into the library. + * + * @param label + * The tape to check for. + * + * @return Whether or not a tape of that name exists + */ + public boolean hasTape(final String label) { + return tapes.containsKey(label); + } + + @Override + public int hashCode() { + final int prime = 31; + + int result = 1; + result = prime * result + (currentTape == null ? 0 : currentTape.hashCode()); + result = prime * result + (tapes == null ? 0 : tapes.hashCode()); + + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof TapeLibrary<?>)) return false; + + final TapeLibrary<?> other = (TapeLibrary<?>) obj; + + if (currentTape == null) { + if (other.currentTape != null) return false; + } else if (!currentTape.equals(other.currentTape)) return false; + + if (tapes == null) { + if (other.tapes != null) return false; + } else if (!tapes.equals(other.tapes)) return false; + + return true; + } + + @Override + public String toString() { + return String.format("TapeLibrary [tapes=%s, currentTape='%s']", tapes, currentTape); + } +} diff --git a/base/src/main/java/bjc/utils/esodata/UnifiedDirectory.java b/base/src/main/java/bjc/utils/esodata/UnifiedDirectory.java new file mode 100644 index 0000000..ffb639f --- /dev/null +++ b/base/src/main/java/bjc/utils/esodata/UnifiedDirectory.java @@ -0,0 +1,105 @@ +package bjc.utils.esodata; + +import bjc.utils.funcdata.FunctionalMap; +import bjc.utils.funcdata.IMap; + +/** + * Simple implementation of {@link Directory}. + * + * Has a unified namespace for data and children. + * + * @author EVE + * + * @param <K> + * The key type of the directory. + * @param <V> + * The value type of the directory. + */ +public class UnifiedDirectory<K, V> implements Directory<K, V> { + private final IMap<K, Directory<K, V>> children; + + private final IMap<K, V> data; + + /** + * Create a new directory. + */ + public UnifiedDirectory() { + children = new FunctionalMap<>(); + data = new FunctionalMap<>(); + } + + @Override + public Directory<K, V> getSubdirectory(final K key) { + return children.get(key); + } + + @Override + public boolean hasSubdirectory(final K key) { + return children.containsKey(key); + } + + @Override + public Directory<K, V> putSubdirectory(final K key, final Directory<K, V> val) { + if (data.containsKey(key)) { + final String msg = String.format("Key %s is already used for data", key); + + throw new IllegalArgumentException(msg); + } + + return children.put(key, val); + } + + @Override + public boolean containsKey(final K key) { + return data.containsKey(key); + } + + @Override + public V getKey(final K key) { + return data.get(key); + } + + @Override + public V putKey(final K key, final V val) { + if (children.containsKey(key)) { + final String msg = String.format("Key %s is already used for sub-directories.", key); + + throw new IllegalArgumentException(msg); + } + + return data.put(key, val); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (children == null ? 0 : children.hashCode()); + result = prime * result + (data == null ? 0 : data.hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof UnifiedDirectory<?, ?>)) return false; + + final UnifiedDirectory<?, ?> other = (UnifiedDirectory<?, ?>) obj; + + if (children == null) { + if (other.children != null) return false; + } else if (!children.equals(other.children)) return false; + + if (data == null) { + if (other.data != null) return false; + } else if (!data.equals(other.data)) return false; + + return true; + } + + @Override + public String toString() { + return String.format("UnifiedDirectory [children=%s, data=%s]", children, data); + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/exceptions/FileNotChosenException.java b/base/src/main/java/bjc/utils/exceptions/FileNotChosenException.java new file mode 100644 index 0000000..6f5a68a --- /dev/null +++ b/base/src/main/java/bjc/utils/exceptions/FileNotChosenException.java @@ -0,0 +1,31 @@ +package bjc.utils.exceptions; + +import java.io.IOException; + +/** + * Represents the user failing to choose a file. + * + * @author ben + * + */ +public class FileNotChosenException extends IOException { + // Version ID for serialization + private static final long serialVersionUID = -8753348705210831096L; + + /** + * Create a new exception + */ + public FileNotChosenException() { + super(); + } + + /** + * Create a new exception with the given cause + * + * @param cause + * The cause of why the exception was thrown + */ + public FileNotChosenException(final String cause) { + super(cause); + } +} diff --git a/base/src/main/java/bjc/utils/exceptions/PragmaFormatException.java b/base/src/main/java/bjc/utils/exceptions/PragmaFormatException.java new file mode 100644 index 0000000..1ad339d --- /dev/null +++ b/base/src/main/java/bjc/utils/exceptions/PragmaFormatException.java @@ -0,0 +1,31 @@ +package bjc.utils.exceptions; + +import java.util.InputMismatchException; + +/** + * The exception to throw whenever a pragma is used with invalid syntax + * + * @author ben + * + */ +public class PragmaFormatException extends InputMismatchException { + // Version ID for serialization + private static final long serialVersionUID = 1288536477368021069L; + + /** + * Create a new exception + */ + public PragmaFormatException() { + super(); + } + + /** + * Create a new exception with the given message + * + * @param message + * The message to explain why the exception was thrown + */ + public PragmaFormatException(final String message) { + super(message); + } +} diff --git a/base/src/main/java/bjc/utils/exceptions/UnknownPragmaException.java b/base/src/main/java/bjc/utils/exceptions/UnknownPragmaException.java new file mode 100644 index 0000000..6fc9113 --- /dev/null +++ b/base/src/main/java/bjc/utils/exceptions/UnknownPragmaException.java @@ -0,0 +1,25 @@ +package bjc.utils.exceptions; + +import java.util.InputMismatchException; + +/** + * Represents a error from encountering a unknown pragma + * + * @author ben + * + */ +public class UnknownPragmaException extends InputMismatchException { + // Version ID for serialization + private static final long serialVersionUID = -4277573484926638662L; + + /** + * Create a new exception with the given cause + * + * @param cause + * The cause for throwing this exception + */ + public UnknownPragmaException(final String cause) { + super(cause); + } + +} diff --git a/base/src/main/java/bjc/utils/funcdata/ExtendedMap.java b/base/src/main/java/bjc/utils/funcdata/ExtendedMap.java new file mode 100644 index 0000000..909c5e9 --- /dev/null +++ b/base/src/main/java/bjc/utils/funcdata/ExtendedMap.java @@ -0,0 +1,127 @@ +package bjc.utils.funcdata; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +import bjc.utils.funcutils.ListUtils; + +class ExtendedMap<KeyType, ValueType> implements IMap<KeyType, ValueType> { + private final IMap<KeyType, ValueType> delegate; + + private final IMap<KeyType, ValueType> store; + + public ExtendedMap(final IMap<KeyType, ValueType> delegate, final IMap<KeyType, ValueType> store) { + this.delegate = delegate; + this.store = store; + } + + @Override + public void clear() { + store.clear(); + } + + @Override + public boolean containsKey(final KeyType key) { + if (store.containsKey(key)) return true; + + return delegate.containsKey(key); + } + + @Override + public IMap<KeyType, ValueType> extend() { + return new ExtendedMap<>(this, new FunctionalMap<>()); + } + + @Override + public void forEach(final BiConsumer<KeyType, ValueType> action) { + store.forEach(action); + + delegate.forEach(action); + } + + @Override + public void forEachKey(final Consumer<KeyType> action) { + store.forEachKey(action); + + delegate.forEachKey(action); + } + + @Override + public void forEachValue(final Consumer<ValueType> action) { + store.forEachValue(action); + + delegate.forEachValue(action); + } + + @Override + public ValueType get(final KeyType key) { + if (store.containsKey(key)) return store.get(key); + + return delegate.get(key); + } + + @Override + public int size() { + return store.size() + delegate.size(); + } + + @Override + public IList<KeyType> keyList() { + return ListUtils.mergeLists(store.keyList(), delegate.keyList()); + } + + @Override + public <MappedValue> IMap<KeyType, MappedValue> transform(final Function<ValueType, MappedValue> transformer) { + return new TransformedValueMap<>(this, transformer); + } + + @Override + public ValueType put(final KeyType key, final ValueType val) { + return store.put(key, val); + } + + @Override + public ValueType remove(final KeyType key) { + if (!store.containsKey(key)) return delegate.remove(key); + + return store.remove(key); + } + + @Override + public IList<ValueType> valueList() { + return ListUtils.mergeLists(store.valueList(), delegate.valueList()); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (delegate == null ? 0 : delegate.hashCode()); + result = prime * result + (store == null ? 0 : store.hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof ExtendedMap)) return false; + + final ExtendedMap<?, ?> other = (ExtendedMap<?, ?>) obj; + + if (delegate == null) { + if (other.delegate != null) return false; + } else if (!delegate.equals(other.delegate)) return false; + if (store == null) { + if (other.store != null) return false; + } else if (!store.equals(other.store)) return false; + + return true; + } + + @Override + public String toString() { + return String.format("ExtendedMap [delegate=%s, store=%s]", delegate, store); + } +} diff --git a/base/src/main/java/bjc/utils/funcdata/FunctionalList.java b/base/src/main/java/bjc/utils/funcdata/FunctionalList.java new file mode 100644 index 0000000..55ea7ff --- /dev/null +++ b/base/src/main/java/bjc/utils/funcdata/FunctionalList.java @@ -0,0 +1,423 @@ +package bjc.utils.funcdata; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +import bjc.utils.data.IHolder; +import bjc.utils.data.IPair; +import bjc.utils.data.Identity; +import bjc.utils.data.Pair; + +/** + * A wrapper over another list that provides eager functional operations over + * it. + * + * Differs from a stream in every way except for the fact that they both provide + * functional operations. + * + * @author ben + * + * @param <E> + * The type in this list + */ +public class FunctionalList<E> implements Cloneable, IList<E> { + /* + * The list used as a backing store + */ + private final List<E> wrapped; + + /** + * Create a new empty functional list. + */ + public FunctionalList() { + wrapped = new ArrayList<>(); + } + + /** + * Create a new functional list containing the specified items. + * + * Takes O(n) time, where n is the number of items specified + * + * @param items + * The items to put into this functional list. + */ + @SafeVarargs + public FunctionalList(final E... items) { + wrapped = new ArrayList<>(items.length); + + for (final E item : items) { + wrapped.add(item); + } + } + + /** + * Create a new functional list with the specified size. + * + * @param size + * The size of the backing list . + */ + private FunctionalList(final int size) { + wrapped = new ArrayList<>(size); + } + + /** + * Create a new functional list as a wrapper of a existing list. + * + * Takes O(1) time, since it doesn't copy the list. + * + * @param backing + * The list to use as a backing list. + */ + public FunctionalList(final List<E> backing) { + if (backing == null) throw new NullPointerException("Backing list must be non-null"); + + wrapped = backing; + } + + @Override + public boolean add(final E item) { + return wrapped.add(item); + } + + @Override + public boolean allMatch(final Predicate<E> predicate) { + if (predicate == null) throw new NullPointerException("Predicate must be non-null"); + + for (final E item : wrapped) { + if (!predicate.test(item)) + // We've found a non-matching item + return false; + } + + // All of the items matched + return true; + } + + @Override + public boolean anyMatch(final Predicate<E> predicate) { + if (predicate == null) throw new NullPointerException("Predicate must be not null"); + + for (final E item : wrapped) { + if (predicate.test(item)) + // We've found a matching item + return true; + } + + // We didn't find a matching item + return false; + } + + /** + * Clone this list into a new one, and clone the backing list as well + * + * Takes O(n) time, where n is the number of elements in the list + * + * @return A list + */ + @Override + public IList<E> clone() { + final IList<E> cloned = new FunctionalList<>(); + + for (final E element : wrapped) { + cloned.add(element); + } + + return cloned; + } + + @Override + public <T, F> IList<F> combineWith(final IList<T> rightList, final BiFunction<E, T, F> itemCombiner) { + if (rightList == null) + throw new NullPointerException("Target combine list must not be null"); + else if (itemCombiner == null) throw new NullPointerException("Combiner must not be null"); + + final IList<F> returned = new FunctionalList<>(); + + // Get the iterator for the other list + final Iterator<T> rightIterator = rightList.toIterable().iterator(); + + for (final Iterator<E> leftIterator = wrapped.iterator(); leftIterator.hasNext() + && rightIterator.hasNext();) { + // Add the transformed items to the result list + final E leftVal = leftIterator.next(); + final T rightVal = rightIterator.next(); + + returned.add(itemCombiner.apply(leftVal, rightVal)); + } + + return returned; + } + + @Override + public boolean contains(final E item) { + // Check if any items in the list match the provided item + return this.anyMatch(item::equals); + } + + @Override + public E first() { + if (wrapped.size() < 1) + throw new NoSuchElementException("Attempted to get first element of empty list"); + + return wrapped.get(0); + } + + @Override + public <T> IList<T> flatMap(final Function<E, IList<T>> expander) { + if (expander == null) throw new NullPointerException("Expander must not be null"); + + final IList<T> returned = new FunctionalList<>(this.wrapped.size()); + + forEach(element -> { + final IList<T> expandedElement = expander.apply(element); + + if (expandedElement == null) throw new NullPointerException("Expander returned null list"); + + // Add each element to the returned list + expandedElement.forEach(returned::add); + }); + + return returned; + } + + @Override + public void forEach(final Consumer<? super E> action) { + if (action == null) throw new NullPointerException("Action is null"); + + wrapped.forEach(action); + } + + @Override + public void forEachIndexed(final BiConsumer<Integer, E> indexedAction) { + if (indexedAction == null) throw new NullPointerException("Action must not be null"); + + // This is held b/c ref'd variables must be final/effectively + // final + final IHolder<Integer> currentIndex = new Identity<>(0); + + wrapped.forEach((element) -> { + // Call the action with the index and the value + indexedAction.accept(currentIndex.unwrap(index -> index), element); + + // Increment the value + currentIndex.transform((index) -> index + 1); + }); + } + + @Override + public E getByIndex(final int index) { + return wrapped.get(index); + } + + /** + * Get the internal backing list. + * + * @return The backing list this list is based off of. + */ + public List<E> getInternal() { + return wrapped; + } + + @Override + public IList<E> getMatching(final Predicate<E> predicate) { + if (predicate == null) throw new NullPointerException("Predicate must not be null"); + + final IList<E> returned = new FunctionalList<>(); + + wrapped.forEach((element) -> { + if (predicate.test(element)) { + // The item matches, so add it to the returned + // list + returned.add(element); + } + }); + + return returned; + } + + @Override + public int getSize() { + return wrapped.size(); + } + + @Override + public boolean isEmpty() { + return wrapped.isEmpty(); + } + + /* + * Check if a partition has room for another item + */ + private Boolean isPartitionFull(final int numberPerPartition, final IHolder<IList<E>> currentPartition) { + return currentPartition.unwrap((partition) -> partition.getSize() >= numberPerPartition); + } + + @Override + public <T> IList<T> map(final Function<E, T> elementTransformer) { + if (elementTransformer == null) throw new NullPointerException("Transformer must be not null"); + + final IList<T> returned = new FunctionalList<>(this.wrapped.size()); + + forEach(element -> { + // Add the transformed item to the result + returned.add(elementTransformer.apply(element)); + }); + + return returned; + } + + @Override + public <T> IList<IPair<E, T>> pairWith(final IList<T> rightList) { + return combineWith(rightList, Pair<E, T>::new); + } + + @Override + public IList<IList<E>> partition(final int numberPerPartition) { + if (numberPerPartition < 1 || numberPerPartition > wrapped.size()) { + final String fmt = "%s is an invalid partition size. Must be between 1 and %d"; + final String msg = String.format(fmt, numberPerPartition, wrapped.size()); + + throw new IllegalArgumentException(msg); + } + + final IList<IList<E>> returned = new FunctionalList<>(); + + // The current partition being filled + final IHolder<IList<E>> currentPartition = new Identity<>(new FunctionalList<>()); + + this.forEach(element -> { + if (isPartitionFull(numberPerPartition, currentPartition)) { + // Add the partition to the list + returned.add(currentPartition.unwrap(partition -> partition)); + + // Start a new partition + currentPartition.transform(partition -> new FunctionalList<>()); + } else { + // Add the element to the current partition + currentPartition.unwrap(partition -> partition.add(element)); + } + }); + + return returned; + } + + @Override + public void prepend(final E item) { + wrapped.add(0, item); + } + + @Override + public E randItem(final Function<Integer, Integer> rnd) { + if (rnd == null) throw new NullPointerException("Random source must not be null"); + + final int randomIndex = rnd.apply(wrapped.size()); + + return wrapped.get(randomIndex); + } + + @Override + public <T, F> F reduceAux(final T initialValue, final BiFunction<E, T, T> stateAccumulator, + final Function<T, F> resultTransformer) { + if (stateAccumulator == null) + throw new NullPointerException("Accumulator must not be null"); + else if (resultTransformer == null) throw new NullPointerException("Transformer must not be null"); + + // The current collapsed list + final IHolder<T> currentState = new Identity<>(initialValue); + + wrapped.forEach(element -> { + // Accumulate a new value into the state + currentState.transform(state -> stateAccumulator.apply(element, state)); + }); + + // Convert the state to its final value + return currentState.unwrap(resultTransformer); + } + + @Override + public boolean removeIf(final Predicate<E> removePredicate) { + if (removePredicate == null) throw new NullPointerException("Predicate must be non-null"); + + return wrapped.removeIf(removePredicate); + } + + @Override + public void removeMatching(final E desiredElement) { + removeIf(element -> element.equals(desiredElement)); + } + + @Override + public void reverse() { + Collections.reverse(wrapped); + } + + @Override + public E search(final E searchKey, final Comparator<E> comparator) { + // Search our internal list + final int foundIndex = Collections.binarySearch(wrapped, searchKey, comparator); + + if (foundIndex >= 0) // We found a matching element + return wrapped.get(foundIndex); + + // We didn't find an element + return null; + } + + @Override + public void sort(final Comparator<E> comparator) { + // sb.deleteCharAt(sb.length() - 2); + Collections.sort(wrapped, comparator); + } + + @Override + public IList<E> tail() { + return new FunctionalList<>(wrapped.subList(1, getSize())); + } + + @Override + public E[] toArray(final E[] arrType) { + return wrapped.toArray(arrType); + } + + @Override + public Iterable<E> toIterable() { + return wrapped; + } + + @Override + public String toString() { + final int lSize = getSize(); + + if (lSize == 0) return "()"; + + final StringBuilder sb = new StringBuilder("("); + final Iterator<E> itr = toIterable().iterator(); + final E itm = itr.next(); + int i = 0; + + if (lSize == 1) return "(" + itm + ")"; + + for (final E item : toIterable()) { + sb.append(item.toString()); + + if (i < lSize - 1) { + sb.append(", "); + } + + i += 1; + } + + sb.append(")"); + + return sb.toString(); + } +} diff --git a/base/src/main/java/bjc/utils/funcdata/FunctionalMap.java b/base/src/main/java/bjc/utils/funcdata/FunctionalMap.java new file mode 100644 index 0000000..c4f0ff1 --- /dev/null +++ b/base/src/main/java/bjc/utils/funcdata/FunctionalMap.java @@ -0,0 +1,175 @@ +package bjc.utils.funcdata; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +import bjc.utils.data.IPair; + +/** + * Basic implementation of {@link IMap} + * + * @author ben + * + * @param <KeyType> + * The type of the map's keys + * @param <ValueType> + * The type of the map's values + */ +public class FunctionalMap<KeyType, ValueType> implements IMap<KeyType, ValueType> { + private Map<KeyType, ValueType> wrappedMap; + + /** + * Create a new blank functional map + */ + public FunctionalMap() { + wrappedMap = new HashMap<>(); + } + + /** + * Create a new functional map with the specified entries + * + * @param entries + * The entries to put into the map + */ + @SafeVarargs + public FunctionalMap(final IPair<KeyType, ValueType>... entries) { + this(); + + for (final IPair<KeyType, ValueType> entry : entries) { + entry.doWith((key, val) -> { + wrappedMap.put(key, val); + }); + } + } + + /** + * Create a new functional map wrapping the specified map + * + * @param wrap + * The map to wrap + */ + public FunctionalMap(final Map<KeyType, ValueType> wrap) { + if (wrap == null) throw new NullPointerException("Map to wrap must not be null"); + + wrappedMap = wrap; + } + + @Override + public void clear() { + wrappedMap.clear(); + } + + @Override + public boolean containsKey(final KeyType key) { + return wrappedMap.containsKey(key); + } + + @Override + public IMap<KeyType, ValueType> extend() { + return new ExtendedMap<>(this, new FunctionalMap<>()); + } + + @Override + public void forEach(final BiConsumer<KeyType, ValueType> action) { + wrappedMap.forEach(action); + } + + @Override + public void forEachKey(final Consumer<KeyType> action) { + wrappedMap.keySet().forEach(action); + } + + @Override + public void forEachValue(final Consumer<ValueType> action) { + wrappedMap.values().forEach(action); + } + + @Override + public ValueType get(final KeyType key) { + if (key == null) throw new NullPointerException("Key must not be null"); + + if (!wrappedMap.containsKey(key)) { + final String msg = String.format("Key %s is not present in the map", key); + + throw new IllegalArgumentException(msg); + } + + return wrappedMap.get(key); + } + + @Override + public int size() { + return wrappedMap.size(); + } + + @Override + public IList<KeyType> keyList() { + final FunctionalList<KeyType> keys = new FunctionalList<>(); + + wrappedMap.keySet().forEach(key -> { + keys.add(key); + }); + + return keys; + } + + @Override + public <MappedValue> IMap<KeyType, MappedValue> transform(final Function<ValueType, MappedValue> transformer) { + if (transformer == null) throw new NullPointerException("Transformer must not be null"); + + return new TransformedValueMap<>(this, transformer); + } + + @Override + public ValueType put(final KeyType key, final ValueType val) { + if (key == null) throw new NullPointerException("Key must not be null"); + + return wrappedMap.put(key, val); + } + + @Override + public ValueType remove(final KeyType key) { + return wrappedMap.remove(key); + } + + @Override + public String toString() { + return wrappedMap.toString(); + } + + @Override + public IList<ValueType> valueList() { + final FunctionalList<ValueType> values = new FunctionalList<>(); + + wrappedMap.values().forEach(value -> { + values.add(value); + }); + + return values; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (wrappedMap == null ? 0 : wrappedMap.hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof FunctionalMap)) return false; + + final FunctionalMap<?, ?> other = (FunctionalMap<?, ?>) obj; + + if (wrappedMap == null) { + if (other.wrappedMap != null) return false; + } else if (!wrappedMap.equals(other.wrappedMap)) return false; + return true; + } +} diff --git a/base/src/main/java/bjc/utils/funcdata/FunctionalStringTokenizer.java b/base/src/main/java/bjc/utils/funcdata/FunctionalStringTokenizer.java new file mode 100644 index 0000000..e068b46 --- /dev/null +++ b/base/src/main/java/bjc/utils/funcdata/FunctionalStringTokenizer.java @@ -0,0 +1,159 @@ +package bjc.utils.funcdata; + +import java.util.StringTokenizer; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * A string tokenizer that exposes a functional interface + * + * @author ben + * + */ +public class FunctionalStringTokenizer { + /** + * Create a new tokenizer from the specified string. + * + * @param strang + * The string to create a tokenizer from. + * @return A new tokenizer that splits the provided string on spaces. + */ + public static FunctionalStringTokenizer fromString(final String strang) { + if (strang == null) throw new NullPointerException("String to tokenize must be non-null"); + + return new FunctionalStringTokenizer(new StringTokenizer(strang, " ")); + } + + /* + * The string tokenizer being driven + */ + private final StringTokenizer input; + + /** + * Create a functional string tokenizer from a given string + * + * @param inp + * The string to tokenize + */ + public FunctionalStringTokenizer(final String inp) { + if (inp == null) throw new NullPointerException("String to tokenize must be non-null"); + + this.input = new StringTokenizer(inp); + } + + /** + * Create a functional string tokenizer from a given string and set of + * separators + * + * @param input + * The string to tokenize + * @param seperators + * The set of separating tokens to use for splitting + */ + public FunctionalStringTokenizer(final String input, final String seperators) { + if (input == null) + throw new NullPointerException("String to tokenize must not be null"); + else if (seperators == null) throw new NullPointerException("Tokens to split on must not be null"); + + this.input = new StringTokenizer(input, seperators); + } + + /** + * Create a functional string tokenizer from a non-functional one + * + * @param toWrap + * The non-functional string tokenizer to wrap + */ + public FunctionalStringTokenizer(final StringTokenizer toWrap) { + if (toWrap == null) throw new NullPointerException("Wrapped tokenizer must not be null"); + + this.input = toWrap; + } + + /** + * Execute a provided action for each of the remaining tokens + * + * @param action + * The action to execute for each token + */ + public void forEachToken(final Consumer<String> action) { + if (action == null) throw new NullPointerException("Action must not be null"); + + while (input.hasMoreTokens()) { + action.accept(input.nextToken()); + } + } + + /** + * Get the string tokenizer encapsulated by this tokenizer + * + * @return The encapsulated tokenizer + */ + public StringTokenizer getInternal() { + return input; + } + + /** + * Check if this tokenizer has more tokens + * + * @return Whether or not this tokenizer has more tokens + */ + public boolean hasMoreTokens() { + return input.hasMoreTokens(); + } + + /** + * Return the next token from the tokenizer. + * + * Returns null if no more tokens are available + * + * @return The next token from the tokenizer + */ + public String nextToken() { + if (input.hasMoreTokens()) // Return the next available token + return input.nextToken(); + + // Return no token + return null; + } + + /** + * Convert this tokenizer into a list of strings + * + * @return This tokenizer, converted into a list of strings + */ + public IList<String> toList() { + return toList((final String element) -> element); + } + + /** + * Convert the contents of this tokenizer into a list. Consumes all of + * the input from this tokenizer. + * + * @param <E> + * The type of the converted tokens + * + * @param transformer + * The function to use to convert tokens. + * @return A list containing all of the converted tokens. + */ + public <E> IList<E> toList(final Function<String, E> transformer) { + if (transformer == null) throw new NullPointerException("Transformer must not be null"); + + final IList<E> returned = new FunctionalList<>(); + + // Add each token to the list after transforming it + forEachToken(token -> { + final E transformedToken = transformer.apply(token); + + returned.add(transformedToken); + }); + + return returned; + } + + @Override + public String toString() { + return String.format("FunctionalStringTokenizer [input=%s]", input); + } +} diff --git a/base/src/main/java/bjc/utils/funcdata/IList.java b/base/src/main/java/bjc/utils/funcdata/IList.java new file mode 100644 index 0000000..28c09d0 --- /dev/null +++ b/base/src/main/java/bjc/utils/funcdata/IList.java @@ -0,0 +1,416 @@ +package bjc.utils.funcdata; + +import java.util.Comparator; +import java.util.Iterator; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collector; + +import bjc.utils.data.IPair; +import bjc.utils.functypes.ID; + +/** + * A wrapper over another list that provides functional operations over it. + * + * @author ben + * + * @param <ContainedType> + * The type in this list + */ +public interface IList<ContainedType> extends Iterable<ContainedType> { + /** + * Add an item to this list + * + * @param item + * The item to add to this list. + * @return Whether the item was added to the list successfully. + */ + boolean add(ContainedType item); + + /** + * Add all of the elements in the provided list to this list + * + * @param items + * The list of items to add + * @return True if every item was successfully added to the list, false + * otherwise + */ + default boolean addAll(final IList<ContainedType> items) { + return items.map(this::add).anyMatch(bl -> bl == false); + } + + /** + * Add all of the elements in the provided array to this list. + * + * @param items + * The array of items to add. + * + * @return True if every item was successfully added to the list, false + * otherwise. + */ + @SuppressWarnings("unchecked") + default boolean addAll(final ContainedType... items) { + boolean succ = true; + + for (final ContainedType item : items) { + final boolean addSucc = add(item); + + succ = succ ? addSucc : false; + } + + return succ; + } + + /** + * Check if all of the elements of this list match the specified + * predicate. + * + * @param matcher + * The predicate to use for checking. + * @return Whether all of the elements of the list match the specified + * predicate. + */ + boolean allMatch(Predicate<ContainedType> matcher); + + /** + * Check if any of the elements in this list match the specified list. + * + * @param matcher + * The predicate to use for checking. + * @return Whether any element in the list matches the provided + * predicate. + */ + boolean anyMatch(Predicate<ContainedType> matcher); + + /** + * Reduce the contents of this list using a collector + * + * @param <StateType> + * The intermediate accumulation type + * @param <ReducedType> + * The final, reduced type + * @param collector + * The collector to use for reduction + * @return The reduced list + */ + default <StateType, ReducedType> ReducedType collect( + final Collector<ContainedType, StateType, ReducedType> collector) { + final BiConsumer<StateType, ContainedType> accumulator = collector.accumulator(); + + final StateType initial = collector.supplier().get(); + return reduceAux(initial, (value, state) -> { + accumulator.accept(state, value); + + return state; + }, collector.finisher()); + } + + /** + * Combine this list with another one into a new list and merge the + * results. + * + * Works sort of like a combined zip/map over resulting pairs. Does not + * change the underlying list. + * + * NOTE: The returned list will have the length of the shorter of this + * list and the combined one. + * + * @param <OtherType> + * The type of the second list + * @param <CombinedType> + * The type of the combined list + * + * @param list + * The list to combine with + * @param combiner + * The function to use for combining element pairs. + * @return A new list containing the merged pairs of lists. + */ + <OtherType, CombinedType> IList<CombinedType> combineWith(IList<OtherType> list, + BiFunction<ContainedType, OtherType, CombinedType> combiner); + + /** + * Check if the list contains the specified item + * + * @param item + * The item to see if it is contained + * @return Whether or not the specified item is in the list + */ + boolean contains(ContainedType item); + + /** + * Get the first element in the list + * + * @return The first element in this list. + */ + ContainedType first(); + + /** + * Apply a function to each member of the list, then flatten the + * results. + * + * Does not change the underlying list. + * + * @param <MappedType> + * The type of the flattened list + * + * @param expander + * The function to apply to each member of the list. + * @return A new list containing the flattened results of applying the + * provided function. + */ + <MappedType> IList<MappedType> flatMap(Function<ContainedType, IList<MappedType>> expander); + + /** + * Apply a given action for each member of the list + * + * @param action + * The action to apply to each member of the list. + */ + @Override + void forEach(Consumer<? super ContainedType> action); + + /** + * Apply a given function to each element in the list and its index. + * + * @param action + * The function to apply to each element in the list and + * its index. + */ + void forEachIndexed(BiConsumer<Integer, ContainedType> action); + + /** + * Retrieve a value in the list by its index. + * + * @param index + * The index to retrieve a value from. + * @return The value at the specified index in the list. + */ + ContainedType getByIndex(int index); + + /** + * Retrieve a list containing all elements matching a predicate + * + * @param predicate + * The predicate to match by + * @return A list containing all elements that match the predicate + */ + IList<ContainedType> getMatching(Predicate<ContainedType> predicate); + + /** + * Retrieve the size of the wrapped list + * + * @return The size of the wrapped list + */ + int getSize(); + + /** + * Check if this list is empty. + * + * @return Whether or not this list is empty. + */ + boolean isEmpty(); + + /** + * Create a new list by applying the given function to each element in + * the list. + * + * Does not change the underlying list. + * + * @param <MappedType> + * The type of the transformed list + * + * @param transformer + * The function to apply to each element in the list + * @return A new list containing the mapped elements of this list. + */ + <MappedType> IList<MappedType> map(Function<ContainedType, MappedType> transformer); + + /** + * Zip two lists into a list of pairs + * + * @param <OtherType> + * The type of the second list + * + * @param list + * The list to use as the left side of the pair + * @return A list containing pairs of this element and the specified + * list + */ + <OtherType> IList<IPair<ContainedType, OtherType>> pairWith(IList<OtherType> list); + + /** + * Partition this list into a list of sublists + * + * @param partitionSize + * The size of elements to put into each one of the + * sublists + * @return A list partitioned into partitions of size nPerPart + */ + IList<IList<ContainedType>> partition(int partitionSize); + + /** + * Prepend an item to the list + * + * @param item + * The item to prepend to the list + */ + void prepend(ContainedType item); + + /** + * Prepend an array of items to the list. + * + * @param items + * The items to prepend to the list. + */ + @SuppressWarnings("unchecked") + default void prependAll(final ContainedType... items) { + for (final ContainedType item : items) { + prepend(item); + } + } + + /** + * Select a random item from the list, using a default random number + * generator + * + * @return A random item from the list + */ + default ContainedType randItem() { + return randItem(num -> (int) (Math.random() * num)); + } + + /** + * Select a random item from this list, using the provided random number + * generator. + * + * @param rnd + * The random number generator to use. + * @return A random element from this list. + */ + ContainedType randItem(Function<Integer, Integer> rnd); + + /** + * Reduce this list to a single value, using a accumulative approach. + * + * @param <StateType> + * The in-between type of the values + * @param <ReducedType> + * The final value type + * + * @param initial + * The initial value of the accumulative state. + * @param accumulator + * The function to use to combine a list element with the + * accumulative state. + * @param transformer + * The function to use to convert the accumulative state + * into a final result. + * @return A single value condensed from this list and transformed into + * its final state. + */ + <StateType, ReducedType> ReducedType reduceAux(StateType initial, + BiFunction<ContainedType, StateType, StateType> accumulator, + Function<StateType, ReducedType> transformer); + + /** + * Reduce this list to a single value, using a accumulative approach. + * + * @param <StateType> + * The in-between type of the values. + * + * @param initial + * The initial value of the accumulative state. + * + * @param accumulator + * The function to use to combine a list element with the + * accumulative state. + * + * @return A single value condensed from this list. + */ + default <StateType> StateType reduceAux(StateType initial, + BiFunction<ContainedType, StateType, StateType> accumulator) { + return reduceAux(initial, accumulator, ID.id()); + } + + /** + * Remove all elements that match a given predicate + * + * @param predicate + * The predicate to use to determine elements to delete + * @return Whether there was anything that satisfied the predicate + */ + boolean removeIf(Predicate<ContainedType> predicate); + + /** + * Remove all parameters that match a given parameter + * + * @param element + * The object to remove all matching copies of + */ + void removeMatching(ContainedType element); + + /** + * Reverse the contents of this list in place + */ + void reverse(); + + /** + * Perform a binary search for the specified key using the provided + * means of comparing elements. + * + * Since this IS a binary search, the list must have been sorted before + * hand. + * + * @param key + * The key to search for. + * @param comparator + * The way to compare elements for searching. Pass null + * to use the natural ordering for E + * @return The element if it is in this list, or null if it is not. + */ + ContainedType search(ContainedType key, Comparator<ContainedType> comparator); + + /** + * Sort the elements of this list using the provided way of comparing + * elements. + * + * Does change the underlying list. + * + * @param comparator + * The way to compare elements for sorting. Pass null to + * use E's natural ordering + */ + void sort(Comparator<ContainedType> comparator); + + /** + * Get the tail of this list (the list without the first element + * + * @return The list without the first element + */ + IList<ContainedType> tail(); + + /** + * Convert this list into an array + * + * @param type + * The type of array to return + * @return The list, as an array + */ + ContainedType[] toArray(ContainedType[] type); + + /** + * Convert the list into a Iterable + * + * @return An iterable view onto the list + */ + Iterable<ContainedType> toIterable(); + + @Override + default Iterator<ContainedType> iterator() { + return toIterable().iterator(); + } +} diff --git a/base/src/main/java/bjc/utils/funcdata/IMap.java b/base/src/main/java/bjc/utils/funcdata/IMap.java new file mode 100644 index 0000000..0ee7375 --- /dev/null +++ b/base/src/main/java/bjc/utils/funcdata/IMap.java @@ -0,0 +1,188 @@ +package bjc.utils.funcdata; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Functional wrapper over map providing some useful things. + * + * @author ben + * + * @param <KeyType> + * The type of this map's keys. + * + * @param <ValueType> + * The type of this map's values. + */ +public interface IMap<KeyType, ValueType> { + /** + * Execute an action for each entry in the map. + * + * @param action + * the action to execute for each entry in the map. + */ + void forEach(BiConsumer<KeyType, ValueType> action); + + /** + * Perform an action for each key in the map. + * + * @param action + * The action to perform on each key in the map. + */ + default void forEachKey(final Consumer<KeyType> action) { + forEach((key, val) -> action.accept(key)); + } + + /** + * Perform an action for each value in the map. + * + * @param action + * The action to perform on each value in the map. + */ + default void forEachValue(final Consumer<ValueType> action) { + forEach((key, val) -> action.accept(val)); + } + + /** + * Check if this map contains the specified key. + * + * @param key + * The key to check. + * + * @return Whether or not the map contains the key. + */ + boolean containsKey(KeyType key); + + /** + * Get the value assigned to the given key. + * + * @param key + * The key to look for a value under. + * + * @return The value of the key. + */ + ValueType get(KeyType key); + + /** + * Get a value from the map, and return a default value if the key + * doesn't exist. + * + * @param key + * The key to attempt to retrieve. + * + * @param defaultValue + * The value to return if the key doesn't exist. + * + * @return The value associated with the key, or the default value if + * the key doesn't exist. + */ + default ValueType getOrDefault(final KeyType key, final ValueType defaultValue) { + try { + return get(key); + } catch (final IllegalArgumentException iaex) { + /* + * We don't care about this, because it indicates a key + * is missing. + */ + return defaultValue; + } + } + + /** + * Add an entry to the map. + * + * @param key + * The key to put the value under. + * + * @param val + * The value to add. + * + * @return The previous value of the key in the map, or null if the key + * wasn't in the map. However, note that it may also return null + * if the key was set to null. + * + * @throws UnsupportedOperationException + * if the map implementation doesn't support modifying + * the map + */ + ValueType put(KeyType key, ValueType val); + + /** + * Delete all the values in the map. + */ + default void clear() { + keyList().forEach(key -> remove(key)); + } + + /** + * Get the number of entries in this map. + * + * @return The number of entries in this map. + */ + default int size() { + return keyList().getSize(); + } + + /** + * Transform the values returned by this map. + * + * NOTE: This transform is applied once for each lookup of a value, so + * the transform passed should be a proper function, or things will + * likely not work as expected. + * + * @param <V2> + * The new type of returned values. + * + * @param transformer + * The function to use to transform values. + * + * @return The map where each value will be transformed after lookup. + */ + default <V2> IMap<KeyType, V2> transform(final Function<ValueType, V2> transformer) { + return new TransformedValueMap<>(this, transformer); + } + + /** + * Extends this map, creating a new map that will delegate queries to + * the map, but store any added values itself. + * + * @return An extended map. + */ + IMap<KeyType, ValueType> extend(); + + /** + * Remove the value bound to the key. + * + * @param key + * The key to remove from the map. + * + * @return The previous value for the key in the map, or null if the key + * wasn't in the class. NOTE: Just because you received null, + * doesn't mean the map wasn't changed. It may mean that someone + * put a null value for that key into the map. + */ + ValueType remove(KeyType key); + + /** + * Get a list of all the keys in this map. + * + * @return A list of all the keys in this map. + */ + IList<KeyType> keyList(); + + /** + * Get a list of the values in this map. + * + * @return A list of values in this map. + */ + default IList<ValueType> valueList() { + final IList<ValueType> returns = new FunctionalList<>(); + + for (final KeyType key : keyList()) { + returns.add(get(key)); + } + + return returns; + } +} diff --git a/base/src/main/java/bjc/utils/funcdata/SentryList.java b/base/src/main/java/bjc/utils/funcdata/SentryList.java new file mode 100644 index 0000000..c322743 --- /dev/null +++ b/base/src/main/java/bjc/utils/funcdata/SentryList.java @@ -0,0 +1,41 @@ +package bjc.utils.funcdata; + +import java.util.List; + +/** + * A list that logs when items are inserted into it. + * + * @author bjculkin + * + * @param <T> + * The type of item in the list. + */ +public class SentryList<T> extends FunctionalList<T> { + /** + * Create a new sentry list. + */ + public SentryList() { + super(); + } + + /** + * Create a new sentry list backed by an existing list. + * + * @param backing + * The backing list. + */ + public SentryList(final List<T> backing) { + super(backing); + } + + @Override + public boolean add(final T item) { + final boolean val = super.add(item); + + if (val) { + System.out.println("Added item (" + item + ") to list"); + } + + return val; + } +} diff --git a/base/src/main/java/bjc/utils/funcdata/TransformedValueMap.java b/base/src/main/java/bjc/utils/funcdata/TransformedValueMap.java new file mode 100644 index 0000000..0ca1fdc --- /dev/null +++ b/base/src/main/java/bjc/utils/funcdata/TransformedValueMap.java @@ -0,0 +1,102 @@ +package bjc.utils.funcdata; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * A map that transforms values from one type to another + * + * @author ben + * + * @param <OldKey> + * The type of the map's keys + * @param <OldValue> + * The type of the map's values + * @param <NewValue> + * The type of the transformed values + */ +final class TransformedValueMap<OldKey, OldValue, NewValue> implements IMap<OldKey, NewValue> { + private final IMap<OldKey, OldValue> backing; + private final Function<OldValue, NewValue> transformer; + + public TransformedValueMap(final IMap<OldKey, OldValue> backingMap, + final Function<OldValue, NewValue> transform) { + backing = backingMap; + transformer = transform; + } + + @Override + public void clear() { + backing.clear(); + } + + @Override + public boolean containsKey(final OldKey key) { + return backing.containsKey(key); + } + + @Override + public IMap<OldKey, NewValue> extend() { + return new ExtendedMap<>(this, new FunctionalMap<>()); + } + + @Override + public void forEach(final BiConsumer<OldKey, NewValue> action) { + backing.forEach((key, value) -> { + action.accept(key, transformer.apply(value)); + }); + } + + @Override + public void forEachKey(final Consumer<OldKey> action) { + backing.forEachKey(action); + } + + @Override + public void forEachValue(final Consumer<NewValue> action) { + backing.forEachValue(value -> { + action.accept(transformer.apply(value)); + }); + } + + @Override + public NewValue get(final OldKey key) { + return transformer.apply(backing.get(key)); + } + + @Override + public int size() { + return backing.size(); + } + + @Override + public IList<OldKey> keyList() { + return backing.keyList(); + } + + @Override + public <MappedValue> IMap<OldKey, MappedValue> transform(final Function<NewValue, MappedValue> transform) { + return new TransformedValueMap<>(this, transform); + } + + @Override + public NewValue put(final OldKey key, final NewValue value) { + throw new UnsupportedOperationException("Can't add items to transformed map"); + } + + @Override + public NewValue remove(final OldKey key) { + return transformer.apply(backing.remove(key)); + } + + @Override + public String toString() { + return backing.toString(); + } + + @Override + public IList<NewValue> valueList() { + return backing.valueList().map(transformer); + } +} diff --git a/base/src/main/java/bjc/utils/funcdata/bst/BinarySearchTree.java b/base/src/main/java/bjc/utils/funcdata/bst/BinarySearchTree.java new file mode 100644 index 0000000..8acd477 --- /dev/null +++ b/base/src/main/java/bjc/utils/funcdata/bst/BinarySearchTree.java @@ -0,0 +1,221 @@ +package bjc.utils.funcdata.bst; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.function.Predicate; + +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.IList; + +/** + * A binary search tree, with some mild support for functional traversal. + * + * @author ben + * + * @param <T> + * The data type stored in the node. + */ +public class BinarySearchTree<T> { + /* + * The comparator for use in ordering items + */ + private final Comparator<T> comparator; + + /* + * The current count of elements in the tree + */ + private int elementCount; + + /* + * The root element of the tree + */ + private ITreePart<T> root; + + /** + * Create a new tree using the specified way to compare elements. + * + * @param cmp + * The thing to use for comparing elements + */ + public BinarySearchTree(final Comparator<T> cmp) { + if (cmp == null) throw new NullPointerException("Comparator must not be null"); + + elementCount = 0; + comparator = cmp; + } + + /** + * Add a node to the binary search tree. + * + * @param element + * The data to add to the binary search tree. + */ + public void addNode(final T element) { + elementCount++; + + if (root == null) { + root = new BinarySearchTreeNode<>(element, null, null); + } else { + root.add(element, comparator); + } + } + + /** + * Check if an adjusted pivot falls with the bounds of a list + * + * @param elements + * The list to get bounds from + * @param pivot + * The pivot + * @param pivotAdjustment + * The distance from the pivot + * @return Whether the adjusted pivot is with the list + */ + private boolean adjustedPivotInBounds(final IList<T> elements, final int pivot, final int pivotAdjustment) { + return pivot - pivotAdjustment >= 0 && pivot + pivotAdjustment < elements.getSize(); + } + + /** + * Balance the tree, and remove soft-deleted nodes for free. + * + * Takes O(N) time, but also O(N) space. + */ + public void balance() { + final IList<T> elements = new FunctionalList<>(); + + // Add each element to the list in sorted order + root.forEach(TreeLinearizationMethod.INORDER, element -> elements.add(element)); + + // Clear the tree + root = null; + + // Set up the pivot and adjustment for readding elements + final int pivot = elements.getSize() / 2; + int pivotAdjustment = 0; + + // Add elements until there aren't any left + while (adjustedPivotInBounds(elements, pivot, pivotAdjustment)) { + if (root == null) { + // Create a new root element + root = new BinarySearchTreeNode<>(elements.getByIndex(pivot), null, null); + } else { + // Add the left and right elements in a balanced + // manner + root.add(elements.getByIndex(pivot + pivotAdjustment), comparator); + + root.add(elements.getByIndex(pivot - pivotAdjustment), comparator); + } + + // Increase the distance from the pivot + pivotAdjustment++; + } + + // Add any trailing unbalanced elements + if (pivot - pivotAdjustment >= 0) { + root.add(elements.getByIndex(pivot - pivotAdjustment), comparator); + } else if (pivot + pivotAdjustment < elements.getSize()) { + root.add(elements.getByIndex(pivot + pivotAdjustment), comparator); + } + } + + /** + * Soft-delete a node from the tree. + * + * Soft-deleted nodes stay in the tree until trim()/balance() is + * invoked, and are not included in traversals/finds. + * + * @param element + * The node to delete + */ + public void deleteNode(final T element) { + elementCount--; + + root.delete(element, comparator); + } + + /** + * Get the root of the tree. + * + * @return The root of the tree. + */ + public ITreePart<T> getRoot() { + return root; + } + + /** + * Check if a node is in the tree + * + * @param element + * The node to check the presence of for the tree. + * @return Whether or not the node is in the tree. + */ + public boolean isInTree(final T element) { + return root.contains(element, comparator); + } + + /** + * Traverse the tree in a specified way until the function fails + * + * @param linearizationMethod + * The way to linearize the tree for traversal + * @param traversalPredicate + * The function to use until it fails + */ + public void traverse(final TreeLinearizationMethod linearizationMethod, final Predicate<T> traversalPredicate) { + if (linearizationMethod == null) + throw new NullPointerException("Linearization method must not be null"); + else if (traversalPredicate == null) throw new NullPointerException("Predicate must not be nulls"); + + root.forEach(linearizationMethod, traversalPredicate); + } + + /** + * Remove all soft-deleted nodes from the tree. + */ + public void trim() { + final List<T> nodes = new ArrayList<>(elementCount); + + // Add all non-soft deleted nodes to the tree in insertion order + traverse(TreeLinearizationMethod.PREORDER, node -> { + nodes.add(node); + return true; + }); + + // Clear the tree + root = null; + + // Add the nodes to the tree in the order they were inserted + nodes.forEach(node -> addNode(node)); + } + + @Override + public String toString() { + return String.format("BinarySearchTree [elementCount=%s, root='%s']", elementCount, root); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + elementCount; + result = prime * result + (root == null ? 0 : root.hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof BinarySearchTree<?>)) return false; + + final BinarySearchTree<?> other = (BinarySearchTree<?>) obj; + + if (elementCount != other.elementCount) return false; + if (root == null) { + if (other.root != null) return false; + } else if (!root.equals(other.root)) return false; + + return true; + } +} diff --git a/base/src/main/java/bjc/utils/funcdata/bst/BinarySearchTreeLeaf.java b/base/src/main/java/bjc/utils/funcdata/bst/BinarySearchTreeLeaf.java new file mode 100644 index 0000000..8c4f3f0 --- /dev/null +++ b/base/src/main/java/bjc/utils/funcdata/bst/BinarySearchTreeLeaf.java @@ -0,0 +1,119 @@ +package bjc.utils.funcdata.bst; + +import java.util.Comparator; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * A leaf in a tree. + * + * @author ben + * + * @param <T> + * The data stored in the tree. + */ +public class BinarySearchTreeLeaf<T> implements ITreePart<T> { + /** + * The data held in this tree leaf + */ + protected T data; + + /** + * Whether this node is soft-deleted or not + */ + protected boolean isDeleted; + + /** + * Create a new leaf holding the specified data. + * + * @param element + * The data for the leaf to hold. + */ + public BinarySearchTreeLeaf(final T element) { + data = element; + } + + @Override + public void add(final T element, final Comparator<T> comparator) { + throw new IllegalArgumentException("Can't add to a leaf."); + } + + @Override + public <E> E collapse(final Function<T, E> leafTransformer, final BiFunction<E, E, E> branchCollapser) { + if (leafTransformer == null) throw new NullPointerException("Transformer must not be null"); + + return leafTransformer.apply(data); + } + + @Override + public boolean contains(final T element, final Comparator<T> comparator) { + return this.data.equals(element); + } + + @Override + public T data() { + return data; + } + + @Override + public void delete(final T element, final Comparator<T> comparator) { + if (data.equals(element)) { + isDeleted = true; + } + } + + @Override + public boolean directedWalk(final DirectedWalkFunction<T> treeWalker) { + if (treeWalker == null) throw new NullPointerException("Tree walker must not be null"); + + switch (treeWalker.walk(data)) { + case SUCCESS: + return true; + // We don't have any children to care about + case FAILURE: + case LEFT: + case RIGHT: + default: + return false; + } + } + + @Override + public boolean forEach(final TreeLinearizationMethod linearizationMethod, + final Predicate<T> traversalPredicate) { + if (traversalPredicate == null) throw new NullPointerException("Predicate must not be null"); + + return traversalPredicate.test(data); + } + + @Override + public String toString() { + return String.format("BinarySearchTreeLeaf [data='%s', isDeleted=%s]", data, isDeleted); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (data == null ? 0 : data.hashCode()); + result = prime * result + (isDeleted ? 1231 : 1237); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof BinarySearchTreeLeaf<?>)) return false; + + final BinarySearchTreeLeaf<?> other = (BinarySearchTreeLeaf<?>) obj; + + if (data == null) { + if (other.data != null) return false; + } else if (!data.equals(other.data)) return false; + if (isDeleted != other.isDeleted) return false; + + return true; + } +} diff --git a/base/src/main/java/bjc/utils/funcdata/bst/BinarySearchTreeNode.java b/base/src/main/java/bjc/utils/funcdata/bst/BinarySearchTreeNode.java new file mode 100644 index 0000000..9f45c17 --- /dev/null +++ b/base/src/main/java/bjc/utils/funcdata/bst/BinarySearchTreeNode.java @@ -0,0 +1,287 @@ +package bjc.utils.funcdata.bst; + +import static bjc.utils.funcdata.bst.DirectedWalkFunction.DirectedWalkResult.FAILURE; +import static bjc.utils.funcdata.bst.DirectedWalkFunction.DirectedWalkResult.LEFT; +import static bjc.utils.funcdata.bst.DirectedWalkFunction.DirectedWalkResult.RIGHT; +import static bjc.utils.funcdata.bst.DirectedWalkFunction.DirectedWalkResult.SUCCESS; + +import java.util.Comparator; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * A binary node in a tree. + * + * @author ben + * + * @param <T> + * The data type stored in the tree. + */ +public class BinarySearchTreeNode<T> extends BinarySearchTreeLeaf<T> { + /* + * The left child of this node + */ + private ITreePart<T> left; + + /* + * The right child of this node + */ + private ITreePart<T> right; + + /** + * Create a new node with the specified data and children. + * + * @param element + * The data to store in this node. + * @param lft + * The left child of this node. + * @param rght + * The right child of this node. + */ + public BinarySearchTreeNode(final T element, final ITreePart<T> lft, final ITreePart<T> rght) { + super(element); + this.left = lft; + this.right = rght; + } + + @Override + public void add(final T element, final Comparator<T> comparator) { + if (comparator == null) throw new NullPointerException("Comparator must not be null"); + + switch (comparator.compare(data, element)) { + case -1: + if (left == null) { + left = new BinarySearchTreeNode<>(element, null, null); + } else { + left.add(element, comparator); + } + break; + case 0: + if (isDeleted) { + isDeleted = false; + } else throw new IllegalArgumentException("Can't add duplicate values"); + break; + case 1: + if (right == null) { + right = new BinarySearchTreeNode<>(element, null, null); + } else { + right.add(element, comparator); + } + break; + default: + throw new IllegalStateException("Error: Comparator yielded invalid value"); + } + } + + @Override + public <E> E collapse(final Function<T, E> nodeCollapser, final BiFunction<E, E, E> branchCollapser) { + if (nodeCollapser == null || branchCollapser == null) + throw new NullPointerException("Collapser must not be null"); + + final E collapsedNode = nodeCollapser.apply(data); + + if (left != null) { + final E collapsedLeftBranch = left.collapse(nodeCollapser, branchCollapser); + + if (right != null) { + final E collapsedRightBranch = right.collapse(nodeCollapser, branchCollapser); + + final E collapsedBranches = branchCollapser.apply(collapsedLeftBranch, + collapsedRightBranch); + + return branchCollapser.apply(collapsedNode, collapsedBranches); + } + + return branchCollapser.apply(collapsedNode, collapsedLeftBranch); + } + + if (right != null) { + final E collapsedRightBranch = right.collapse(nodeCollapser, branchCollapser); + + return branchCollapser.apply(collapsedNode, collapsedRightBranch); + } + + return collapsedNode; + } + + @Override + public boolean contains(final T element, final Comparator<T> comparator) { + if (comparator == null) throw new NullPointerException("Comparator must not be null"); + + return directedWalk(currentElement -> { + switch (comparator.compare(element, currentElement)) { + case -1: + return LEFT; + case 0: + return isDeleted ? FAILURE : SUCCESS; + case 1: + return RIGHT; + default: + return FAILURE; + } + }); + } + + @Override + public void delete(final T element, final Comparator<T> comparator) { + if (comparator == null) throw new NullPointerException("Comparator must not be null"); + + directedWalk(currentElement -> { + switch (comparator.compare(data, element)) { + case -1: + return left == null ? FAILURE : LEFT; + case 0: + isDeleted = true; + return FAILURE; + case 1: + return right == null ? FAILURE : RIGHT; + default: + return FAILURE; + } + }); + } + + @Override + public boolean directedWalk(final DirectedWalkFunction<T> treeWalker) { + if (treeWalker == null) throw new NullPointerException("Walker must not be null"); + + switch (treeWalker.walk(data)) { + case SUCCESS: + return true; + case LEFT: + return left.directedWalk(treeWalker); + case RIGHT: + return right.directedWalk(treeWalker); + case FAILURE: + return false; + default: + return false; + } + } + + @Override + public boolean forEach(final TreeLinearizationMethod linearizationMethod, + final Predicate<T> traversalPredicate) { + if (linearizationMethod == null) + throw new NullPointerException("Linearization method must not be null"); + else if (traversalPredicate == null) throw new NullPointerException("Predicate must not be null"); + + switch (linearizationMethod) { + case PREORDER: + return preorderTraverse(linearizationMethod, traversalPredicate); + case INORDER: + return inorderTraverse(linearizationMethod, traversalPredicate); + case POSTORDER: + return postorderTraverse(linearizationMethod, traversalPredicate); + default: + throw new IllegalArgumentException( + "Passed an incorrect TreeLinearizationMethod " + linearizationMethod + ". WAT"); + } + } + + private boolean inorderTraverse(final TreeLinearizationMethod linearizationMethod, + final Predicate<T> traversalPredicate) { + if (!traverseLeftBranch(linearizationMethod, traversalPredicate)) return false; + + if (!traverseElement(traversalPredicate)) return false; + + if (!traverseRightBranch(linearizationMethod, traversalPredicate)) return false; + + return true; + } + + private boolean postorderTraverse(final TreeLinearizationMethod linearizationMethod, + final Predicate<T> traversalPredicate) { + if (!traverseLeftBranch(linearizationMethod, traversalPredicate)) return false; + + if (!traverseRightBranch(linearizationMethod, traversalPredicate)) return false; + + if (!traverseElement(traversalPredicate)) return false; + + return true; + + } + + private boolean preorderTraverse(final TreeLinearizationMethod linearizationMethod, + final Predicate<T> traversalPredicate) { + if (!traverseElement(traversalPredicate)) return false; + + if (!traverseLeftBranch(linearizationMethod, traversalPredicate)) return false; + + if (!traverseRightBranch(linearizationMethod, traversalPredicate)) return false; + + return true; + } + + private boolean traverseElement(final Predicate<T> traversalPredicate) { + boolean nodeSuccesfullyTraversed; + + if (isDeleted) { + nodeSuccesfullyTraversed = true; + } else { + nodeSuccesfullyTraversed = traversalPredicate.test(data); + } + + return nodeSuccesfullyTraversed; + } + + private boolean traverseLeftBranch(final TreeLinearizationMethod linearizationMethod, + final Predicate<T> traversalPredicate) { + boolean leftSuccesfullyTraversed; + + if (left == null) { + leftSuccesfullyTraversed = true; + } else { + leftSuccesfullyTraversed = left.forEach(linearizationMethod, traversalPredicate); + } + + return leftSuccesfullyTraversed; + } + + private boolean traverseRightBranch(final TreeLinearizationMethod linearizationMethod, + final Predicate<T> traversalPredicate) { + boolean rightSuccesfullyTraversed; + + if (right == null) { + rightSuccesfullyTraversed = true; + } else { + rightSuccesfullyTraversed = right.forEach(linearizationMethod, traversalPredicate); + } + + return rightSuccesfullyTraversed; + } + + @Override + public String toString() { + return String.format("BinarySearchTreeNode [left='%s', right='%s']", left, right); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + (left == null ? 0 : left.hashCode()); + result = prime * result + (right == null ? 0 : right.hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (!super.equals(obj)) return false; + if (!(obj instanceof BinarySearchTreeNode<?>)) return false; + + final BinarySearchTreeNode<?> other = (BinarySearchTreeNode<?>) obj; + + if (left == null) { + if (other.left != null) return false; + } else if (!left.equals(other.left)) return false; + + if (right == null) { + if (other.right != null) return false; + } else if (!right.equals(other.right)) return false; + + return true; + } +} diff --git a/base/src/main/java/bjc/utils/funcdata/bst/DirectedWalkFunction.java b/base/src/main/java/bjc/utils/funcdata/bst/DirectedWalkFunction.java new file mode 100644 index 0000000..e11524a --- /dev/null +++ b/base/src/main/java/bjc/utils/funcdata/bst/DirectedWalkFunction.java @@ -0,0 +1,49 @@ +package bjc.utils.funcdata.bst; + +/** + * Represents a function for doing a directed walk of a binary tree. + * + * @author ben + * + * @param <T> + * The type of element stored in the walked tree + */ +@FunctionalInterface +public interface DirectedWalkFunction<T> { + /** + * Represents the results used to direct a walk in a binary tree. + * + * @author ben + * + */ + public enum DirectedWalkResult { + /** + * Specifies that the function has failed. + */ + FAILURE, + /** + * Specifies that the function wants to move left in the tree + * next. + */ + LEFT, + /** + * Specifies that the function wants to move right in the tree + * next. + */ + RIGHT, + /** + * Specifies that the function has succesfully completed + * + */ + SUCCESS + } + + /** + * Perform a directed walk on a node of a tree. + * + * @param element + * The data stored in the node currently being visited + * @return The way the function wants the walk to go next. + */ + public DirectedWalkResult walk(T element); +} diff --git a/base/src/main/java/bjc/utils/funcdata/bst/ITreePart.java b/base/src/main/java/bjc/utils/funcdata/bst/ITreePart.java new file mode 100644 index 0000000..3aa8880 --- /dev/null +++ b/base/src/main/java/bjc/utils/funcdata/bst/ITreePart.java @@ -0,0 +1,96 @@ +package bjc.utils.funcdata.bst; + +import java.util.Comparator; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * A interface for the fundamental things that want to be part of a tree. + * + * @author ben + * + * @param <T> + * The data contained in this part of the tree. + */ +public interface ITreePart<T> { + /** + * Add a element below this tree part somewhere. + * + * @param element + * The element to add below this tree part + * @param comparator + * The thing to use for comparing values to find where to + * insert the tree part. + */ + public void add(T element, Comparator<T> comparator); + + /** + * Collapses this tree part into a single value. Does not change the + * underlying tree. + * + * @param <E> + * The type of the final collapsed value + * + * @param nodeCollapser + * The function to use to transform data into mapped + * form. + * @param branchCollapser + * The function to use to collapse data in mapped form + * into a single value. + * @return A single value from collapsing the tree. + */ + public <E> E collapse(Function<T, E> nodeCollapser, BiFunction<E, E, E> branchCollapser); + + /** + * Check if this tre part or below it contains the specified data item + * + * @param element + * The data item to look for. + * @param comparator + * The comparator to use to search for the data item + * @return Whether or not the given item is contained in this tree part + * or its children. + */ + public boolean contains(T element, Comparator<T> comparator); + + /** + * Get the data associated with this tree part. + * + * @return The data associated with this tree part. + */ + public T data(); + + /** + * Remove the given node from this tree part and any of its children. + * + * @param element + * The data item to remove. + * @param comparator + * The comparator to use to search for the data item. + */ + public void delete(T element, Comparator<T> comparator); + + /** + * Execute a directed walk through the tree. + * + * @param walker + * The function to use to direct the walk through the + * tree. + * @return Whether the directed walk finished successfully. + */ + public boolean directedWalk(DirectedWalkFunction<T> walker); + + /** + * Execute a provided function for each element of tree it succesfully + * completes for + * + * @param linearizationMethod + * The way to linearize the tree for executing + * @param predicate + * The predicate to apply to each element, where it + * returning false terminates traversal early + * @return Whether the traversal finished succesfully + */ + public boolean forEach(TreeLinearizationMethod linearizationMethod, Predicate<T> predicate); +} diff --git a/base/src/main/java/bjc/utils/funcdata/bst/TreeLinearizationMethod.java b/base/src/main/java/bjc/utils/funcdata/bst/TreeLinearizationMethod.java new file mode 100644 index 0000000..0c83867 --- /dev/null +++ b/base/src/main/java/bjc/utils/funcdata/bst/TreeLinearizationMethod.java @@ -0,0 +1,25 @@ +package bjc.utils.funcdata.bst; + +/** + * Represents the ways to linearize a tree for traversal. + * + * @author ben + * + */ +public enum TreeLinearizationMethod { + /** + * Visit the left side of this tree part, the tree part itself, and then + * the right part. + */ + INORDER, + /** + * Visit the left side of this tree part, the right side, and then the + * tree part itself. + */ + POSTORDER, + /** + * Visit the tree part itself, then the left side of tthis tree part and + * then the right part. + */ + PREORDER +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/funcdata/theory/Bifunctor.java b/base/src/main/java/bjc/utils/funcdata/theory/Bifunctor.java new file mode 100644 index 0000000..13c1709 --- /dev/null +++ b/base/src/main/java/bjc/utils/funcdata/theory/Bifunctor.java @@ -0,0 +1,139 @@ +package bjc.utils.funcdata.theory; + +import java.util.function.Function; + +/** + * A functor over a pair of heterogeneous types + * + * @author ben + * @param <LeftType> + * The type stored on the 'left' of the pair + * @param <RightType> + * The type stored on the 'right' of the pair + * + */ +public interface Bifunctor<LeftType, RightType> { + /** + * Alias for functor mapping. + * + * @author EVE + * + * @param <OldLeft> + * @param <OldRight> + * @param <NewLeft> + * @param <NewRight> + */ + public interface BifunctorMap<OldLeft, OldRight, NewLeft, NewRight> + extends Function<Bifunctor<OldLeft, OldRight>, Bifunctor<NewLeft, NewRight>> { + + } + + /** + * Alias for left functor mapping. + * + * @author EVE + * + * @param <OldLeft> + * @param <OldRight> + * @param <NewLeft> + */ + public interface LeftBifunctorMap<OldLeft, OldRight, NewLeft> + extends BifunctorMap<OldLeft, OldRight, NewLeft, OldRight> { + + } + + /** + * Alias for right functor mapping. + * + * @author EVE + * + * @param <OldLeft> + * @param <OldRight> + * @param <NewRight> + */ + public interface RightBifunctorMap<OldLeft, OldRight, NewRight> + extends BifunctorMap<OldLeft, OldRight, OldLeft, NewRight> { + + } + + /** + * Lift a pair of functions to a single function that maps over both + * parts of a pair + * + * @param <OldLeft> + * The old left type of the pair + * @param <OldRight> + * The old right type of the pair + * @param <NewLeft> + * The new left type of the pair + * @param <NewRight> + * The new right type of the pair + * @param leftFunc + * The function that maps over the left of the pair + * @param rightFunc + * The function that maps over the right of the pair + * @return A function that maps over both parts of the pair + */ + public default <OldLeft, OldRight, NewLeft, NewRight> BifunctorMap<OldLeft, OldRight, NewLeft, NewRight> bimap( + final Function<OldLeft, NewLeft> leftFunc, final Function<OldRight, NewRight> rightFunc) { + final BifunctorMap<OldLeft, OldRight, NewLeft, NewRight> bimappedFunc = (argPair) -> { + final LeftBifunctorMap<OldLeft, OldRight, NewLeft> leftMapper = argPair.fmapLeft(leftFunc); + + final Bifunctor<NewLeft, OldRight> leftMappedFunctor = leftMapper.apply(argPair); + final RightBifunctorMap<NewLeft, OldRight, NewRight> rightMapper = leftMappedFunctor + .fmapRight(rightFunc); + + return rightMapper.apply(leftMappedFunctor); + }; + + return bimappedFunc; + } + + /** + * Lift a function to operate over the left part of this pair + * + * @param <OldLeft> + * The old left type of the pair + * @param <OldRight> + * The old right type of the pair + * @param <NewLeft> + * The new left type of the pair + * @param func + * The function to lift to work over the left side of the + * pair + * @return The function lifted to work over the left side of bifunctors + */ + public <OldLeft, OldRight, NewLeft> LeftBifunctorMap<OldLeft, OldRight, NewLeft> fmapLeft( + Function<OldLeft, NewLeft> func); + + /** + * Lift a function to operate over the right part of this pair + * + * @param <OldLeft> + * The old left type of the pair + * @param <OldRight> + * The old right type of the pair + * @param <NewRight> + * The new right type of the pair + * @param func + * The function to lift to work over the right side of + * the pair + * @return The function lifted to work over the right side of bifunctors + */ + public <OldLeft, OldRight, NewRight> RightBifunctorMap<OldLeft, OldRight, NewRight> fmapRight( + Function<OldRight, NewRight> func); + + /** + * Get the value contained on the left of this bifunctor + * + * @return The value on the left side of this bifunctor + */ + public LeftType getLeft(); + + /** + * Get the value contained on the right of this bifunctor + * + * @return The value on the right of this bifunctor + */ + public RightType getRight(); +} diff --git a/base/src/main/java/bjc/utils/funcdata/theory/Functor.java b/base/src/main/java/bjc/utils/funcdata/theory/Functor.java new file mode 100644 index 0000000..1c53284 --- /dev/null +++ b/base/src/main/java/bjc/utils/funcdata/theory/Functor.java @@ -0,0 +1,39 @@ +package bjc.utils.funcdata.theory; + +import java.util.function.Function; + +/** + * Represents a container or context some sort usually, but the precise + * definition is that it represents exactly what it is defined as + * + * @author ben + * @param <ContainedType> + * The value inside the functor + */ +public interface Functor<ContainedType> { + /** + * Converts a normal function to operate over values in a functor. + * + * N.B: Even though the type signature implies that you can apply the + * resulting function to any type of functor, it is only safe to call it + * on instances of the type of functor you called fmap on. + * + * @param <ArgType> + * The argument of the function + * @param <ReturnType> + * The return type of the function + * @param func + * The function to convert + * @return The passed in function converted to work over a particular + * type of functors + */ + public <ArgType, ReturnType> Function<Functor<ArgType>, Functor<ReturnType>> fmap( + Function<ArgType, ReturnType> func); + + /** + * Retrieve the thing inside this functor + * + * @return The thing inside this functor + */ + public ContainedType getValue(); +} diff --git a/base/src/main/java/bjc/utils/functypes/ID.java b/base/src/main/java/bjc/utils/functypes/ID.java new file mode 100644 index 0000000..d3197e2 --- /dev/null +++ b/base/src/main/java/bjc/utils/functypes/ID.java @@ -0,0 +1,20 @@ +package bjc.utils.functypes;
+
+import java.util.function.UnaryOperator;
+
+/**
+ * Identity function.
+ *
+ * @author bjculkin
+ *
+ */
+public class ID {
+ /**
+ * Create an identity function.
+ *
+ * @return A identity function.
+ */
+ public static <A> UnaryOperator<A> id() {
+ return (x) -> x;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/functypes/ListFlattener.java b/base/src/main/java/bjc/utils/functypes/ListFlattener.java new file mode 100644 index 0000000..cfa0c8b --- /dev/null +++ b/base/src/main/java/bjc/utils/functypes/ListFlattener.java @@ -0,0 +1,17 @@ +package bjc.utils.functypes; + +import java.util.function.Function; + +import bjc.utils.funcdata.IList; + +/** + * A function that flattens a list. + * + * @author bjculkin + * + * @param <S> + * The type of value in the list. + */ +public interface ListFlattener<S> extends Function<IList<S>, S> { + +} diff --git a/base/src/main/java/bjc/utils/funcutils/CollectorUtils.java b/base/src/main/java/bjc/utils/funcutils/CollectorUtils.java new file mode 100644 index 0000000..a044bfd --- /dev/null +++ b/base/src/main/java/bjc/utils/funcutils/CollectorUtils.java @@ -0,0 +1,39 @@ +package bjc.utils.funcutils; + +import java.util.stream.Collector; + +import bjc.utils.data.IHolder; +import bjc.utils.data.IPair; + +/** + * Utilities for producing implementations of {@link Collector} + * + * @author ben + * + */ +public class CollectorUtils { + /** + * Create a collector that applies two collectors at once + * + * @param <InitialType> + * The type of the collection to collect from + * @param <AuxType1> + * The intermediate type of the first collector + * @param <AuxType2> + * The intermediate type of the second collector + * @param <FinalType1> + * The final type of the first collector + * @param <FinalType2> + * The final type of the second collector + * @param first + * The first collector to use + * @param second + * The second collector to use + * @return A collector that functions as mentioned above + */ + public static <InitialType, AuxType1, AuxType2, FinalType1, FinalType2> Collector<InitialType, IHolder<IPair<AuxType1, AuxType2>>, IPair<FinalType1, FinalType2>> compoundCollect( + final Collector<InitialType, AuxType1, FinalType1> first, + final Collector<InitialType, AuxType2, FinalType2> second) { + return new CompoundCollector<>(first, second); + } +} diff --git a/base/src/main/java/bjc/utils/funcutils/CompoundCollector.java b/base/src/main/java/bjc/utils/funcutils/CompoundCollector.java new file mode 100644 index 0000000..35695bc --- /dev/null +++ b/base/src/main/java/bjc/utils/funcutils/CompoundCollector.java @@ -0,0 +1,89 @@ +package bjc.utils.funcutils; + +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; + +import bjc.utils.data.IHolder; +import bjc.utils.data.IPair; +import bjc.utils.data.Identity; +import bjc.utils.data.Pair; + +final class CompoundCollector<InitialType, AuxType1, AuxType2, FinalType1, FinalType2> + implements Collector<InitialType, IHolder<IPair<AuxType1, AuxType2>>, IPair<FinalType1, FinalType2>> { + + private final Set<java.util.stream.Collector.Characteristics> characteristicSet; + + private final Collector<InitialType, AuxType1, FinalType1> first; + private final Collector<InitialType, AuxType2, FinalType2> second; + + public CompoundCollector(final Collector<InitialType, AuxType1, FinalType1> first, + final Collector<InitialType, AuxType2, FinalType2> second) { + this.first = first; + this.second = second; + + characteristicSet = first.characteristics(); + characteristicSet.addAll(second.characteristics()); + } + + @Override + public BiConsumer<IHolder<IPair<AuxType1, AuxType2>>, InitialType> accumulator() { + final BiConsumer<AuxType1, InitialType> firstAccumulator = first.accumulator(); + final BiConsumer<AuxType2, InitialType> secondAccumulator = second.accumulator(); + + return (state, value) -> { + state.doWith(statePair -> { + statePair.doWith((left, right) -> { + firstAccumulator.accept(left, value); + secondAccumulator.accept(right, value); + }); + }); + }; + } + + @Override + public Set<java.util.stream.Collector.Characteristics> characteristics() { + return characteristicSet; + } + + @Override + public BinaryOperator<IHolder<IPair<AuxType1, AuxType2>>> combiner() { + final BinaryOperator<AuxType1> firstCombiner = first.combiner(); + final BinaryOperator<AuxType2> secondCombiner = second.combiner(); + + return (leftState, rightState) -> { + return leftState.unwrap(leftPair -> { + return rightState.transform(rightPair -> { + return leftPair.combine(rightPair, firstCombiner, secondCombiner); + }); + }); + }; + } + + @Override + public Function<IHolder<IPair<AuxType1, AuxType2>>, IPair<FinalType1, FinalType2>> finisher() { + return state -> { + return state.unwrap(pair -> { + return pair.bind((left, right) -> { + final FinalType1 finalLeft = first.finisher().apply(left); + final FinalType2 finalRight = second.finisher().apply(right); + + return new Pair<>(finalLeft, finalRight); + }); + }); + }; + } + + @Override + public Supplier<IHolder<IPair<AuxType1, AuxType2>>> supplier() { + return () -> { + final AuxType1 initialLeft = first.supplier().get(); + final AuxType2 initialRight = second.supplier().get(); + + return new Identity<>(new Pair<>(initialLeft, initialRight)); + }; + } +} diff --git a/base/src/main/java/bjc/utils/funcutils/EnumUtils.java b/base/src/main/java/bjc/utils/funcutils/EnumUtils.java new file mode 100644 index 0000000..e4c0bda --- /dev/null +++ b/base/src/main/java/bjc/utils/funcutils/EnumUtils.java @@ -0,0 +1,63 @@ +package bjc.utils.funcutils; + +import java.util.Random; +import java.util.function.Consumer; + +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.IList; + +/** + * Utility methods on enums + * + * @author ben + * + */ +public class EnumUtils { + /** + * Do an action for a random number of enum values + * + * @param <E> + * The type of the enum + * @param clasz + * The enum class + * @param nValues + * The number of values to execute the action on + * @param action + * The action to perform on random values + * @param rnd + * The source of randomness to use + */ + public static <E extends Enum<E>> void doForValues(final Class<E> clasz, final int nValues, + final Consumer<E> action, final Random rnd) { + final E[] enumValues = clasz.getEnumConstants(); + + final IList<E> valueList = new FunctionalList<>(enumValues); + + final int randomValueCount = enumValues.length - nValues; + + for (int i = 0; i <= randomValueCount; i++) { + final E rDir = valueList.randItem(rnd::nextInt); + + valueList.removeMatching(rDir); + } + + valueList.forEach(action); + } + + /** + * Get a random value from an enum + * + * @param <E> + * The type of the enum + * @param clasz + * The class of the enum + * @param rnd + * The random source to use + * @return A random value from the specified enum + */ + public static <E extends Enum<E>> E getRandomValue(final Class<E> clasz, final Random rnd) { + final E[] enumValues = clasz.getEnumConstants(); + + return new FunctionalList<>(enumValues).randItem(rnd::nextInt); + } +} diff --git a/base/src/main/java/bjc/utils/funcutils/FileUtils.java b/base/src/main/java/bjc/utils/funcutils/FileUtils.java new file mode 100644 index 0000000..87199b1 --- /dev/null +++ b/base/src/main/java/bjc/utils/funcutils/FileUtils.java @@ -0,0 +1,40 @@ +package bjc.utils.funcutils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.function.BiPredicate; + +/** + * Utilities for doing things with files + * + * @author ben + * + */ +public class FileUtils { + /** + * Traverse a directory recursively. This is a depth-first traversal + * + * + * @param root + * The directory to start the traversal at + * @param predicate + * The predicate to determine whether or not to traverse + * a directory + * @param action + * The action to invoke upon each file in the directory. + * Returning true means to continue the traversal, + * returning false stops it + * @throws IOException + * if the walk throws an exception + * + * TODO If it becomes necessary, write another overload + * for this with all the buttons and knobs from + * walkFileTree + */ + public static void traverseDirectory(final Path root, final BiPredicate<Path, BasicFileAttributes> predicate, + final BiPredicate<Path, BasicFileAttributes> action) throws IOException { + Files.walkFileTree(root, new FunctionalFileVisitor(predicate, action)); + } +} diff --git a/base/src/main/java/bjc/utils/funcutils/FuncUtils.java b/base/src/main/java/bjc/utils/funcutils/FuncUtils.java new file mode 100644 index 0000000..9950add --- /dev/null +++ b/base/src/main/java/bjc/utils/funcutils/FuncUtils.java @@ -0,0 +1,76 @@ +package bjc.utils.funcutils; + +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +/** + * Utility things for functions + * + * @author ben + * + */ +public class FuncUtils { + /** + * Convert a binary function into a unary function that returns a + * function + * + * @param <A> + * The initial type of the function + * @param <B> + * The intermediate type of the function + * @param <C> + * The terminal type of the function + * @param func + * The function to transform + * @return The function transformed into a unary function returning a + * function + */ + public static <A, B, C> Function<A, Function<B, C>> curry2(final BiFunction<A, B, C> func) { + return arg1 -> arg2 -> { + return func.apply(arg1, arg2); + }; + } + + /** + * Do the specified action the specified number of times + * + * @param nTimes + * The number of times to do the action + * @param cons + * The action to perform + */ + public static void doTimes(final int nTimes, final Consumer<Integer> cons) { + for (int i = 0; i < nTimes; i++) { + cons.accept(i); + } + } + + /** + * Return an operator that executes until it converges. + * + * @param op + * The operator to execute. + * @param maxTries + * The maximum amount of times to apply the function in an + * attempt to cause it to converge. + */ + public static <T> UnaryOperator<T> converge(final UnaryOperator<T> op, final int maxTries) { + return (val) -> { + T newVal = op.apply(val); + T oldVal; + + int tries = 0; + + do { + oldVal = newVal; + newVal = op.apply(newVal); + + tries += 1; + } while(!newVal.equals(oldVal) && tries < maxTries); + + return newVal; + }; + } +} diff --git a/base/src/main/java/bjc/utils/funcutils/FunctionalFileVisitor.java b/base/src/main/java/bjc/utils/funcutils/FunctionalFileVisitor.java new file mode 100644 index 0000000..db6c43b --- /dev/null +++ b/base/src/main/java/bjc/utils/funcutils/FunctionalFileVisitor.java @@ -0,0 +1,36 @@ +package bjc.utils.funcutils; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.function.BiPredicate; + +/* + * Functional implementation of a file visitor. + */ +final class FunctionalFileVisitor extends SimpleFileVisitor<Path> { + private final BiPredicate<Path, BasicFileAttributes> predicate; + private final BiPredicate<Path, BasicFileAttributes> action; + + public FunctionalFileVisitor(final BiPredicate<Path, BasicFileAttributes> predicate, + final BiPredicate<Path, BasicFileAttributes> action) { + this.predicate = predicate; + this.action = action; + } + + @Override + public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) throws IOException { + if (predicate.test(dir, attrs)) return FileVisitResult.CONTINUE; + + return FileVisitResult.SKIP_SUBTREE; + } + + @Override + public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException { + if (action.test(file, attrs)) return FileVisitResult.CONTINUE; + + return FileVisitResult.TERMINATE; + } +} diff --git a/base/src/main/java/bjc/utils/funcutils/GroupPartIteration.java b/base/src/main/java/bjc/utils/funcutils/GroupPartIteration.java new file mode 100644 index 0000000..f3b2254 --- /dev/null +++ b/base/src/main/java/bjc/utils/funcutils/GroupPartIteration.java @@ -0,0 +1,62 @@ +package bjc.utils.funcutils; + +import java.util.function.Consumer; +import java.util.function.Function; + +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.IList; + +/** + * Implements a single group partitioning pass on a list + * + * @author ben + * + * @param <E> + * The type of element in the list being partitioned + */ +final class GroupPartIteration<E> implements Consumer<E> { + private final IList<IList<E>> returnedList; + + public IList<E> currentPartition; + private final IList<E> rejectedItems; + + private int numberInCurrentPartition; + private final int numberPerPartition; + + private final Function<E, Integer> elementCounter; + + public GroupPartIteration(final IList<IList<E>> returned, final IList<E> rejects, final int nPerPart, + final Function<E, Integer> eleCount) { + this.returnedList = returned; + this.rejectedItems = rejects; + this.numberPerPartition = nPerPart; + this.elementCounter = eleCount; + + this.currentPartition = new FunctionalList<>(); + this.numberInCurrentPartition = 0; + } + + @Override + public void accept(final E value) { + final boolean shouldStartPartition = numberInCurrentPartition >= numberPerPartition; + + if (shouldStartPartition) { + returnedList.add(currentPartition); + + currentPartition = new FunctionalList<>(); + numberInCurrentPartition = 0; + } else { + final int currentElementCount = elementCounter.apply(value); + + final boolean shouldReject = numberInCurrentPartition + + currentElementCount >= numberPerPartition; + + if (shouldReject) { + rejectedItems.add(value); + } else { + currentPartition.add(value); + numberInCurrentPartition += currentElementCount; + } + } + } +} diff --git a/base/src/main/java/bjc/utils/funcutils/IBuilder.java b/base/src/main/java/bjc/utils/funcutils/IBuilder.java new file mode 100644 index 0000000..a96a4d6 --- /dev/null +++ b/base/src/main/java/bjc/utils/funcutils/IBuilder.java @@ -0,0 +1,31 @@ +package bjc.utils.funcutils; + +/** + * Generic interface for objects that implement the builder pattern + * + * @author ben + * + * @param <E> + * The type of object being built + */ +public interface IBuilder<E> { + /** + * Build the object this builder is building + * + * @return The built object + * @throws IllegalStateException + * if the data in the builder cannot be built into its + * corresponding object at this point in time + */ + public E build(); + + /** + * Reset the state of this builder to its initial state + * + * @throws UnsupportedOperationException + * if the builder doesn't support resetting its state + */ + public default void reset() { + throw new UnsupportedOperationException("Builder doesn't support state resetting"); + } +} diff --git a/base/src/main/java/bjc/utils/funcutils/Isomorphism.java b/base/src/main/java/bjc/utils/funcutils/Isomorphism.java new file mode 100644 index 0000000..2d3655e --- /dev/null +++ b/base/src/main/java/bjc/utils/funcutils/Isomorphism.java @@ -0,0 +1,60 @@ +package bjc.utils.funcutils; + +import java.util.function.Function; + +/** + * A pair of functions to transform between a pair of types. + * + * @author bjculkin + * + * @param <S> + * The source type of the isomorphism. + * + * @param <D> + * The destination type of isomorphism. + * + */ +public class Isomorphism<S, D> { + private Function<S, D> toFunc; + private Function<D, S> fromFunc; + + /** + * Create a new isomorphism. + * + * @param to + * The 'forward' function, from the source to the + * definition. + * + * @param from + * The 'backward' function, from the definition to the + * source. + */ + public Isomorphism(Function<S, D> to, Function<D, S> from) { + toFunc = to; + fromFunc = from; + } + + /** + * Apply the isomorphism forward. + * + * @param val + * The source value. + * + * @return The destination value. + */ + public D to(S val) { + return toFunc.apply(val); + } + + /** + * Apply the isomorphism backward. + * + * @param val + * The destination value. + * + * @return The source value. + */ + public S from(D val) { + return fromFunc.apply(val); + } +} diff --git a/base/src/main/java/bjc/utils/funcutils/LambdaLock.java b/base/src/main/java/bjc/utils/funcutils/LambdaLock.java new file mode 100644 index 0000000..62c5d32 --- /dev/null +++ b/base/src/main/java/bjc/utils/funcutils/LambdaLock.java @@ -0,0 +1,105 @@ +package bjc.utils.funcutils; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Supplier; + +/** + * A wrapper around a {@link ReadWriteLock} to ensure that the lock is used + * properly. + * + * @author EVE + * + */ +public class LambdaLock { + private final Lock readLock; + private final Lock writeLock; + + /** + * Create a new lambda-enabled lock around a new lock. + */ + public LambdaLock() { + this(new ReentrantReadWriteLock()); + } + + /** + * Create a new lambda-enabled lock. + * + * @param lck + * The lock to wrap. + */ + public LambdaLock(final ReadWriteLock lck) { + readLock = lck.readLock(); + writeLock = lck.writeLock(); + } + + /** + * Execute an action with the read lock taken. + * + * @param supp + * The action to call. + * + * @return The result of the action. + */ + public <T> T read(final Supplier<T> supp) { + readLock.lock(); + + try { + return supp.get(); + } finally { + readLock.unlock(); + } + } + + /** + * Execute an action with the write lock taken. + * + * @param supp + * The action to call. + * + * @return The result of the action. + */ + public <T> T write(final Supplier<T> supp) { + writeLock.lock(); + + try { + return supp.get(); + } finally { + writeLock.unlock(); + } + } + + /** + * Execute an action with the read lock taken. + * + * @param action + * The action to call. + * + */ + public void read(final Runnable action) { + readLock.lock(); + + try { + action.run(); + } finally { + readLock.unlock(); + } + } + + /** + * Execute an action with the write lock taken. + * + * @param action + * The action to call. + */ + public void write(final Runnable action) { + writeLock.lock(); + + try { + action.run(); + } finally { + writeLock.unlock(); + } + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/funcutils/ListUtils.java b/base/src/main/java/bjc/utils/funcutils/ListUtils.java new file mode 100644 index 0000000..c0daa1e --- /dev/null +++ b/base/src/main/java/bjc/utils/funcutils/ListUtils.java @@ -0,0 +1,294 @@ +package bjc.utils.funcutils; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.function.Function; +import java.util.function.Supplier; + +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.IList; + +/** + * Utilities for manipulating FunctionalLists that don't belong in the class + * itself + * + * @author ben + * + */ +public class ListUtils { + private static final int MAX_NTRIESPART = 50; + + /** + * Collapse a string of tokens into a single string without adding any + * spaces + * + * @param input + * The list of tokens to collapse + * @return The collapsed string of tokens + */ + public static String collapseTokens(final IList<String> input) { + if (input == null) throw new NullPointerException("Input must not be null"); + + return collapseTokens(input, ""); + } + + /** + * Collapse a string of tokens into a single string, adding the desired + * separator after each token + * + * @param input + * The list of tokens to collapse + * @param seperator + * The separator to use for separating tokens + * @return The collapsed string of tokens + */ + public static String collapseTokens(final IList<String> input, final String seperator) { + if (input == null) + throw new NullPointerException("Input must not be null"); + else if (seperator == null) throw new NullPointerException("Seperator must not be null"); + + if (input.getSize() < 1) + return ""; + else if (input.getSize() == 1) + return input.first(); + else { + final StringBuilder state = new StringBuilder(); + + int i = 1; + for (final String itm : input.toIterable()) { + state.append(itm); + + if (i != input.getSize()) { + state.append(seperator); + } + + i += 1; + } + + return state.toString(); + } + } + + /** + * Select a number of random items from the list without replacement + * + * @param <E> + * The type of items to select + * @param list + * The list to select from + * @param number + * The number of items to selet + * @param rng + * A function that creates a random number from 0 to the + * desired number + * @return A new list containing the desired number of items randomly + * selected from the specified list without replacement + */ + + public static <E> IList<E> drawWithoutReplacement(final IList<E> list, final int number, + final Function<Integer, Integer> rng) { + final IList<E> selected = new FunctionalList<>(new ArrayList<>(number)); + + final int total = list.getSize(); + + final Iterator<E> itr = list.toIterable().iterator(); + E element = null; + + for (final int index = 0; itr.hasNext(); element = itr.next()) { + /* + * n - m + */ + final int winningChance = number - selected.getSize(); + + /* + * N - t + */ + final int totalChance = total - (index - 1); + + /* + * Probability of selecting the t+1'th element + */ + if (NumberUtils.isProbable(winningChance, totalChance, rng)) { + selected.add(element); + } + } + + return selected; + } + + /** + * Select a number of random items from the list, with replacement + * + * @param <E> + * The type of items to select + * @param list + * The list to select from + * @param number + * The number of items to selet + * @param rng + * A function that creates a random number from 0 to the + * desired number + * @return A new list containing the desired number of items randomly + * selected from the specified list + */ + public static <E> IList<E> drawWithReplacement(final IList<E> list, final int number, + final Function<Integer, Integer> rng) { + final IList<E> selected = new FunctionalList<>(new ArrayList<>(number)); + + for (int i = 0; i < number; i++) { + selected.add(list.randItem(rng)); + } + + return selected; + } + + /** + * Partition a list into a list of lists, where each element can count + * for more than one element in a partition + * + * @param <E> + * The type of elements in the list to partition + * + * @param input + * The list to partition + * @param counter + * The function to determine the count for each element + * for + * @param partitionSize + * The number of elements to put in each partition + * + * @return A list partitioned according to the above rules + */ + public static <E> IList<IList<E>> groupPartition(final IList<E> input, final Function<E, Integer> counter, + final int partitionSize) { + if (input == null) + throw new NullPointerException("Input list must not be null"); + else if (counter == null) + throw new NullPointerException("Counter must not be null"); + else if (partitionSize < 1 || partitionSize > input.getSize()) { + final String fmt = "%d is not a valid partition size. Must be between 1 and %d"; + final String msg = String.format(fmt, partitionSize, input.getSize()); + + throw new IllegalArgumentException(msg); + } + + /* + * List that holds our results + */ + final IList<IList<E>> returned = new FunctionalList<>(); + + /* + * List that holds elements rejected during current pass + */ + final IList<E> rejected = new FunctionalList<>(); + + final GroupPartIteration<E> it = new GroupPartIteration<>(returned, rejected, partitionSize, counter); + + /* + * Run up to a certain number of passes + */ + for (int numberOfIterations = 0; numberOfIterations < MAX_NTRIESPART + && !rejected.isEmpty(); numberOfIterations++) { + input.forEach(it); + + if (rejected.isEmpty()) { + /* + * Nothing was rejected, so we're done + */ + return returned; + } + } + + + final String fmt = "Heuristic (more than %d iterations of partitioning) detected an unpartitionable list. (%s)\nThe following elements were not partitioned: %s\nCurrent group in formation: %s\nPreviously formed groups: %s\n"; + + final String msg = String.format(fmt, MAX_NTRIESPART, input.toString(), rejected.toString(), it.currentPartition.toString(), returned.toString()); + + throw new IllegalArgumentException(msg); + } + + /** + * Merge the contents of a bunch of lists together into a single list + * + * @param <E> + * The type of value in this lists + * @param lists + * The values in the lists to merge + * @return A list containing all the elements of the lists + */ + @SafeVarargs + public static <E> IList<E> mergeLists(final IList<E>... lists) { + final IList<E> returned = new FunctionalList<>(); + + for (final IList<E> list : lists) { + for (final E itm : list.toIterable()) { + returned.add(itm); + } + } + + return returned; + } + + /** + * Pad the provided list out to the desired size + * + * @param <E> + * The type of elements in the list + * @param list + * The list to pad out + * @param counter + * The function to count elements with + * @param size + * The desired size of the list + * @param padder + * The function to get elements to pad with + * @return The list, padded to the desired size + * @throws IllegalArgumentException + * if the list couldn't be padded to the desired size + */ + public static <E> IList<E> padList(final IList<E> list, final Function<E, Integer> counter, final int size, + final Supplier<E> padder) { + int count = 0; + + final IList<E> returned = new FunctionalList<>(); + + for (final E itm : list.toIterable()) { + count += counter.apply(itm); + + returned.add(itm); + } + + if (count % size != 0) { + /* + * We need to pad + */ + int needed = count % size; + int threshold = 0; + + while (needed > 0 && threshold <= MAX_NTRIESPART) { + final E val = padder.get(); + final int newCount = counter.apply(val); + + if (newCount <= needed) { + returned.add(val); + + threshold = 0; + + needed -= newCount; + } else { + threshold += 1; + } + } + + if (threshold > MAX_NTRIESPART) { + final String fmt = "Heuristic (more than %d iterations of attempting to pad) detected an unpaddable list. (%s)\nPartially padded list: %S"; + + final String msg = String.format(fmt, MAX_NTRIESPART, list.toString(), returned.toString()); + + throw new IllegalArgumentException(msg); + } + } + + return returned; + } +} diff --git a/base/src/main/java/bjc/utils/funcutils/NumberUtils.java b/base/src/main/java/bjc/utils/funcutils/NumberUtils.java new file mode 100644 index 0000000..770d3a5 --- /dev/null +++ b/base/src/main/java/bjc/utils/funcutils/NumberUtils.java @@ -0,0 +1,69 @@ +package bjc.utils.funcutils; + +import java.util.function.Function; + +/** + * Utility functions for dealing with numbers + * + * @author ben + * + */ +public class NumberUtils { + /** + * Compute the falling factorial of a number + * + * @param value + * The number to compute + * @param power + * The power to do the falling factorial for + * @return The falling factorial of the number to the power + */ + public static int fallingFactorial(final int value, final int power) { + if (power == 0) + return 1; + else if (power == 1) + return value; + else { + int result = 1; + + for (int currentSub = 0; currentSub < power + 1; currentSub++) { + result *= value - currentSub; + } + + return result; + } + } + + /** + * Evaluates a linear probability distribution + * + * @param winning + * The number of winning possibilities + * @param total + * The number of total possibilities + * @param rng + * The function to use to generate a random possibility + * @return Whether or not a random possibility was a winning one + */ + public static boolean isProbable(final int winning, final int total, final Function<Integer, Integer> rng) { + return rng.apply(total) < winning; + } + + /** + * Check if a number is in an inclusive range. + * + * @param min + * The minimum value of the range. + * + * @param max + * The maximum value of the range. + * + * @param i + * The number to check. + * + * @return Whether the number is in the range. + */ + public static boolean between(final int min, final int max, final int i) { + return i >= min && i <= max; + } +} diff --git a/base/src/main/java/bjc/utils/funcutils/StringUtils.java b/base/src/main/java/bjc/utils/funcutils/StringUtils.java new file mode 100644 index 0000000..62f78f5 --- /dev/null +++ b/base/src/main/java/bjc/utils/funcutils/StringUtils.java @@ -0,0 +1,196 @@ +package bjc.utils.funcutils; + +import java.util.Deque; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.ibm.icu.text.BreakIterator; + +/** + * Utility methods for operations on strings + * + * @author ben + * + */ +public class StringUtils { + /** + * Check if a string consists only of one or more matches of a regular + * expression + * + * @param input + * The string to check + * @param rRegex + * The regex to see if the string only contains matches + * of + * @return Whether or not the string consists only of multiple matches + * of the provided regex + */ + public static boolean containsOnly(final String input, final String rRegex) { + if (input == null) + throw new NullPointerException("Input must not be null"); + else if (rRegex == null) throw new NullPointerException("Regex must not be null"); + + /* + * This regular expression is fairly simple. + * + * First, we match the beginning of the string. Then, we start a + * non-capturing group whose contents are the passed in regex. + * That group is then matched one or more times and the pattern + * matches to the end of the string + */ + return input.matches("\\A(?:" + rRegex + ")+\\Z"); + } + + /** + * Indent the string being built in a StringBuilder n levels + * + * @param builder + * The builder to indent in + * @param levels + * The number of levels to indent + */ + public static void indentNLevels(final StringBuilder builder, final int levels) { + for (int i = 0; i < levels; i++) { + builder.append("\t"); + } + } + + /** + * Print out a deque with a special case for easily showing a deque is + * empty + * + * @param <ContainedType> + * The type in the deque + * @param queue + * The deque to print + * @return A string version of the deque, with allowance for an empty + * deque + */ + public static <ContainedType> String printDeque(final Deque<ContainedType> queue) { + return queue.isEmpty() ? "(none)" : queue.toString(); + } + + /** + * Converts a sequence to an English list. + * + * @param objects + * The sequence to convert to an English list. + * @param join + * The string to use for separating the last element from + * the rest. + * @param comma + * The string to use as a comma + * + * @return The sequence as an English list. + */ + public static String toEnglishList(final Object[] objects, final String join, final String comma) { + if (objects == null) throw new NullPointerException("Sequence must not be null"); + + final StringBuilder sb = new StringBuilder(); + + final String joiner = join; + final String coma = comma; + + switch (objects.length) { + case 0: + /* + * Empty list. + */ + break; + case 1: + /* + * One item. + */ + sb.append(objects[0].toString()); + break; + case 2: + /* + * Two items. + */ + sb.append(objects[0].toString()); + sb.append(" " + joiner + " "); + sb.append(objects[1].toString()); + break; + default: + /* + * Three or more items. + */ + for (int i = 0; i < objects.length - 1; i++) { + sb.append(objects[i].toString()); + sb.append(coma + " "); + } + /* + * Uncomment this to remove serial commas. + * + * int lc = sb.length() - 1; + * + * sb.delete(lc - coma.length(), lc); + */ + sb.append(joiner + " "); + sb.append(objects[objects.length - 1].toString()); + } + + return sb.toString(); + } + + /** + * Converts a sequence to an English list. + * + * @param objects + * The sequence to convert to an English list. + * @param join + * The string to use for separating the last element from + * the rest. + * + * @return The sequence as an English list. + */ + public static String toEnglishList(final Object[] objects, final String join) { + return toEnglishList(objects, join, ","); + } + + /** + * Converts a sequence to an English list. + * + * @param objects + * The sequence to convert to an English list. + * @param and + * Whether to use 'and' or 'or'. + * + * @return The sequence as an English list. + */ + public static String toEnglishList(final Object[] objects, final boolean and) { + if (and) + return toEnglishList(objects, "and"); + else return toEnglishList(objects, "or"); + } + + /** + * Count the number of graphemes in a string. + * + * @param value + * The string to check. + * + * @return The number of graphemes in the string. + */ + public static int graphemeCount(final String value) { + final BreakIterator it = BreakIterator.getCharacterInstance(); + it.setText(value); + + int count = 0; + while (it.next() != BreakIterator.DONE) { + count++; + } + + return count; + } + + public static int countMatches(final String value, final String pattern) { + Matcher mat = Pattern.compile(pattern).matcher(value); + + int num = 0; + while(mat.find()) + num += 1; + + return num; + } +} diff --git a/base/src/main/java/bjc/utils/funcutils/TreeUtils.java b/base/src/main/java/bjc/utils/funcutils/TreeUtils.java new file mode 100644 index 0000000..dcd5738 --- /dev/null +++ b/base/src/main/java/bjc/utils/funcutils/TreeUtils.java @@ -0,0 +1,56 @@ +package bjc.utils.funcutils; + +import java.util.LinkedList; +import java.util.function.Predicate; + +import bjc.utils.data.ITree; +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.IList; + +/** + * Implements various utilities for trees. + * + * @author Benjamin Culkin + */ +public class TreeUtils { + /* + * Convert a tree into a list of outline nodes that match a certain + * path. + */ + public static <T> IList<IList<T>> outlineTree(ITree<T> tre, Predicate<T> leafMarker) { + IList<IList<T>> paths = new FunctionalList<>(); + + LinkedList<T> path = new LinkedList<>(); + path.add(tre.getHead()); + + tre.doForChildren((child) -> findPath(child, path, leafMarker, paths)); + + return paths; + } + + private static <T> void findPath(ITree<T> subtree, LinkedList<T> path, Predicate<T> leafMarker, IList<IList<T>> paths) { + if(subtree.getChildrenCount() == 0 && leafMarker.test(subtree.getHead())) { + /* + * We're at a matching leaf node. Add it. + */ + IList<T> finalPath = new FunctionalList<>(); + + for(T ePath : path) { + finalPath.add(ePath); + } + + finalPath.add(subtree.getHead()); + + paths.add(finalPath); + } else { + /* + * Check the children of this node. + */ + path.add(subtree.getHead()); + + subtree.doForChildren((child) -> findPath(child, path, leafMarker, paths)); + + path.removeLast(); + } + } +} diff --git a/base/src/main/java/bjc/utils/funcutils/TriConsumer.java b/base/src/main/java/bjc/utils/funcutils/TriConsumer.java new file mode 100644 index 0000000..f30386c --- /dev/null +++ b/base/src/main/java/bjc/utils/funcutils/TriConsumer.java @@ -0,0 +1,31 @@ +package bjc.utils.funcutils; + +/** + * Consumer that takes three arguments. + * + * @author EVE + * + * @param <A> + * Type of the first argument. + * @param <B> + * Type of the second argument. + * @param <C> + * Type of the third argument. + * + */ +@FunctionalInterface +public interface TriConsumer<A, B, C> { + /** + * Perform the action. + * + * @param a + * The first parameter. + * + * @param b + * The second parameter. + * + * @param c + * The third parameter. + */ + public void accept(A a, B b, C c); +} diff --git a/base/src/main/java/bjc/utils/gen/RandomGrammar.java b/base/src/main/java/bjc/utils/gen/RandomGrammar.java new file mode 100644 index 0000000..3de08d6 --- /dev/null +++ b/base/src/main/java/bjc/utils/gen/RandomGrammar.java @@ -0,0 +1,69 @@ +package bjc.utils.gen; + +import bjc.utils.funcdata.FunctionalMap; +import bjc.utils.funcdata.IList; + +/** + * A weighted grammar where all the rules have a equal chance of occuring. + * + * @author ben + * + * @param <E> + * The type of grammar elements to use. + */ +public class RandomGrammar<E> extends WeightedGrammar<E> { + /** + * Create a new random grammar. + */ + public RandomGrammar() { + rules = new FunctionalMap<>(); + } + + /** + * Add cases to a specified rule. + * + * @param rule + * The name of the rule to add cases to. + * @param cases + * The cases to add for this rule. + */ + @SafeVarargs + public final void addCases(final E rule, final IList<E>... cases) { + for (final IList<E> currentCase : cases) { + super.addCase(rule, 1, currentCase); + } + } + + /** + * Create a rule with the specified name and cases. + * + * @param rule + * The name of the rule to add. + * @param cases + * The cases to add for this rule. + */ + @SafeVarargs + public final void makeRule(final E rule, final IList<E>... cases) { + super.addRule(rule); + + for (final IList<E> currentCase : cases) { + super.addCase(rule, 1, currentCase); + } + } + + /** + * Create a rule with the specified name and cases. + * + * @param rule + * The name of the rule to add. + * @param cases + * The cases to add for this rule. + */ + public void makeRule(final E rule, final IList<IList<E>> cases) { + if (cases == null) throw new NullPointerException("Cases must not be null"); + + super.addRule(rule); + + cases.forEach(currentCase -> super.addCase(rule, 1, currentCase)); + } +} diff --git a/base/src/main/java/bjc/utils/gen/WeightedGrammar.java b/base/src/main/java/bjc/utils/gen/WeightedGrammar.java new file mode 100644 index 0000000..7777ad8 --- /dev/null +++ b/base/src/main/java/bjc/utils/gen/WeightedGrammar.java @@ -0,0 +1,573 @@ +package bjc.utils.gen; + +import java.util.Random; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import bjc.utils.data.IPair; +import bjc.utils.data.Pair; +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.FunctionalMap; +import bjc.utils.funcdata.IList; +import bjc.utils.funcdata.IMap; + +/** + * A random grammar, where certain rules will come up more often than others. + * + * @author ben + * + * @param <E> + * The values that make up sentences of this grammar. + */ +public class WeightedGrammar<E> { + /** + * The initial rule of the grammar + */ + protected String initialRule; + + /** + * The rules currently in this grammar + */ + protected IMap<E, WeightedRandom<IList<E>>> rules; + + /** + * The random number generator used for random numbers + */ + private Random rng; + + /** + * All of the subgrammars of this grammar + */ + protected IMap<E, WeightedGrammar<E>> subgrammars; + + /** + * Rules that require special handling + */ + private IMap<E, Supplier<IList<E>>> specialRules; + + /** + * Predicate for marking special tokens + */ + + private Predicate<E> specialMarker; + + /** + * Action for special tokens + */ + private BiFunction<E, WeightedGrammar<E>, IList<E>> specialAction; + + /** + * Create a new weighted grammar. + */ + public WeightedGrammar() { + rules = new FunctionalMap<>(); + subgrammars = new FunctionalMap<>(); + specialRules = new FunctionalMap<>(); + } + + /** + * Create a new weighted grammar that uses the specified source of + * randomness. + * + * @param source + * The source of randomness to use + */ + public WeightedGrammar(final Random source) { + this(); + + if (source == null) throw new NullPointerException("Source of randomness must be non-null"); + + rng = source; + } + + /** + * Configure the action to perform on special tokens. + * + * @param marker + * The marker to find special tokens. + * + * @param action + * The action to take on those tokens. + */ + public void configureSpecial(final Predicate<E> marker, + final BiFunction<E, WeightedGrammar<E>, IList<E>> action) { + specialMarker = marker; + specialAction = action; + } + + /** + * Adds a special rule to the grammar. + * + * @param ruleName + * The name of the special rule. + * + * @param cse + * The case for the rule. + */ + public void addSpecialRule(final E ruleName, final Supplier<IList<E>> cse) { + if (ruleName == null) + throw new NullPointerException("Rule name must not be null"); + else if (cse == null) throw new NullPointerException("Case must not be null"); + + specialRules.put(ruleName, cse); + } + + /** + * Add a case to an already existing rule. + * + * @param ruleName + * The rule to add a case to. + * @param probability + * The probability for this rule to be chosen. + * @param cse + * The case being added. + */ + public void addCase(final E ruleName, final int probability, final IList<E> cse) { + if (ruleName == null) + throw new NullPointerException("Rule name must be not null"); + else if (cse == null) throw new NullPointerException("Case body must not be null"); + + rules.get(ruleName).addProbability(probability, cse); + } + + /** + * Add a alias for an existing subgrammar + * + * @param name + * The name of the subgrammar to alias + * @param alias + * The alias of the subgrammar + * @return Whether the alias was succesfully created + */ + public boolean addGrammarAlias(final E name, final E alias) { + if (name == null) + throw new NullPointerException("Subgrammar name must not be null"); + else if (alias == null) throw new NullPointerException("Subgrammar alias must not be null"); + + if (subgrammars.containsKey(alias)) return false; + + if (subgrammars.containsKey(name)) { + subgrammars.put(alias, subgrammars.get(name)); + return true; + } + + return false; + } + + /** + * Add a new rule with no cases. + * + * @param name + * The name of the rule to add. + * @return Whether or not the rule was successfully added. + */ + public boolean addRule(final E name) { + if (rng == null) { + rng = new Random(); + } + + if (name == null) throw new NullPointerException("Rule name must not be null"); + + return addRule(name, new WeightedRandom<>(rng)); + } + + /** + * Add a new rule with a set of cases. + * + * @param name + * The name of the rule to add. + * @param cases + * The set of cases for the rule. + * @return Whether or not the rule was succesfully added. + */ + public boolean addRule(final E name, final WeightedRandom<IList<E>> cases) { + if (name == null) + throw new NullPointerException("Name must not be null"); + else if (cases == null) throw new NullPointerException("Cases must not be null"); + + if (rules.containsKey(name)) return false; + + rules.put(name, cases); + return true; + } + + /** + * Add a subgrammar. + * + * @param name + * The name of the subgrammar. + * @param subgrammar + * The subgrammar to add. + * @return Whether or not the subgrammar was succesfully added. + */ + public boolean addSubgrammar(final E name, final WeightedGrammar<E> subgrammar) { + if (name == null) + throw new NullPointerException("Subgrammar name must not be null"); + else if (subgrammar == null) throw new NullPointerException("Subgrammar must not be null"); + + if (subgrammars.containsKey(name)) return false; + + subgrammars.put(name, subgrammar); + return true; + } + + /** + * Remove a rule with the specified name. + * + * @param name + * The name of the rule to remove. + */ + public void deleteRule(final E name) { + if (name == null) throw new NullPointerException("Rule name must not be null"); + + rules.remove(name); + } + + /** + * Remove a subgrammar with the specified name. + * + * @param name + * The name of the subgrammar to remove. + */ + public void deleteSubgrammar(final E name) { + if (name == null) throw new NullPointerException("Rule name must not be null"); + + subgrammars.remove(name); + } + + /** + * Generate a set of debug sentences for the specified rule. + * + * Only generates sentences one layer deep. + * + * @param ruleName + * The rule to test. + * @return A set of sentences generated by the specified rule. + */ + public IList<IList<E>> generateDebugValues(final E ruleName) { + if (ruleName == null) throw new NullPointerException("Rule name must not be null"); + + final IList<IList<E>> returnedList = new FunctionalList<>(); + + final WeightedRandom<IList<E>> ruleGenerator = rules.get(ruleName); + + for (int i = 0; i < 10; i++) { + returnedList.add(ruleGenerator.generateValue()); + } + + return returnedList; + } + + /** + * Generate a generic sentence from a initial rule. + * + * @param <T> + * The type of the transformed output + * + * @param initRules + * The initial rule to start with. + * + * @param tokenTransformer + * The function to transform grammar output into + * something. + * + * @param spacer + * The spacer element to add in between output tokens. + * + * @return A randomly generated sentence from the specified initial + * rule. + */ + public <T> IList<T> generateGenericValues(final E initRules, final Function<E, T> tokenTransformer, + final T spacer) { + if (initRules == null) + throw new NullPointerException("Initial rule must not be null"); + else if (tokenTransformer == null) + throw new NullPointerException("Transformer must not be null"); + else if (spacer == null) throw new NullPointerException("Spacer must not be null"); + + final IList<T> returnedList = new FunctionalList<>(); + + IList<E> genRules = new FunctionalList<>(initRules); + + if (specialMarker != null) { + if (specialMarker.test(initRules)) { + genRules = specialAction.apply(initRules, this); + } + } + + for (final E initRule : genRules.toIterable()) { + if (specialRules.containsKey(initRule)) { + for (final E rulePart : specialRules.get(initRule).get().toIterable()) { + final Iterable<T> generatedRuleParts = generateGenericValues(rulePart, + tokenTransformer, spacer).toIterable(); + + for (final T generatedRulePart : generatedRuleParts) { + returnedList.add(generatedRulePart); + returnedList.add(spacer); + } + } + } else if (subgrammars.containsKey(initRule)) { + final Iterable<T> ruleParts = subgrammars.get(initRule) + .generateGenericValues(initRule, tokenTransformer, spacer).toIterable(); + + for (final T rulePart : ruleParts) { + returnedList.add(rulePart); + returnedList.add(spacer); + } + } else if (rules.containsKey(initRule)) { + final Iterable<E> ruleParts = rules.get(initRule).generateValue().toIterable(); + + for (final E rulePart : ruleParts) { + final Iterable<T> generatedRuleParts = generateGenericValues(rulePart, + tokenTransformer, spacer).toIterable(); + + for (final T generatedRulePart : generatedRuleParts) { + returnedList.add(generatedRulePart); + returnedList.add(spacer); + } + } + } else { + final T transformedToken = tokenTransformer.apply(initRule); + + if (transformedToken == null) + throw new NullPointerException("Transformer created null token"); + + returnedList.add(transformedToken); + returnedList.add(spacer); + } + } + + return returnedList; + } + + /** + * Generate a random list of grammar elements from a given initial rule. + * + * @param initRule + * The initial rule to start with. + * @param spacer + * The item to use to space the list. + * @return A list of random grammar elements generated by the specified + * rule. + */ + public IList<E> generateListValues(final E initRule, final E spacer) { + final IList<E> retList = generateGenericValues(initRule, strang -> strang, spacer); + + return retList; + } + + /** + * Get the initial rule of this grammar + * + * @return The initial rule of this grammar + */ + public String getInitialRule() { + return initialRule; + } + + /** + * Returns the number of rules in this grammar + * + * @return The number of rules in this grammar + */ + public int getRuleCount() { + return rules.size(); + } + + /** + * Returns a set containing all of the rules in this grammar + * + * @return The set of all rule names in this grammar + */ + public IList<E> getRuleNames() { + final IList<E> ruleNames = new FunctionalList<>(); + + ruleNames.addAll(rules.keyList()); + ruleNames.addAll(specialRules.keyList()); + + return ruleNames; + } + + /** + * Get the subgrammar with the specified name. + * + * @param name + * The name of the subgrammar to get. + * @return The subgrammar with the specified name. + */ + public WeightedGrammar<E> getSubgrammar(final E name) { + if (name == null) throw new NullPointerException("Subgrammar name must not be null"); + + return subgrammars.get(name); + } + + /** + * Check if this grammar has an initial rule + * + * @return Whether or not this grammar has an initial rule + */ + public boolean hasInitialRule() { + return initialRule != null && !initialRule.equalsIgnoreCase(""); + } + + /** + * Check if this grammar has a given rule. + * + * @param ruleName + * The rule to check for. + * + * @return Whether or not the grammar has a rule by that name. + */ + public boolean hasRule(final E ruleName) { + return rules.containsKey(ruleName) || specialRules.containsKey(ruleName); + } + + /** + * Prefix a given rule with a token multiple times + * + * @param ruleName + * The name of the rule to prefix + * @param prefixToken + * The token to prefix to the rules + * @param additionalProbability + * The additional probability of the tokens + * @param numberOfTimes + * The number of times to prefix the token + */ + public void multiPrefixRule(final E ruleName, final E prefixToken, final int additionalProbability, + final int numberOfTimes) { + if (ruleName == null) + throw new NullPointerException("Rule name must not be null"); + else if (prefixToken == null) + throw new NullPointerException("Prefix token must not be null"); + else if (numberOfTimes < 1) + throw new IllegalArgumentException("Number of times to prefix must be positive."); + + final WeightedRandom<IList<E>> rule = rules.get(ruleName); + + final IList<IPair<Integer, IList<E>>> newResults = new FunctionalList<>(); + + rule.getValues().forEach((pair) -> { + final IList<IList<E>> newRule = new FunctionalList<>(); + + for (int i = 1; i <= numberOfTimes; i++) { + final IList<E> newCase = pair.merge((left, right) -> { + final IList<E> returnVal = new FunctionalList<>(); + + for (final E val : right.toIterable()) { + returnVal.add(val); + } + + return returnVal; + }); + + for (int j = 1; j <= i; j++) { + newCase.prepend(prefixToken); + } + + newRule.add(newCase); + } + + newRule.forEach((list) -> { + final Integer currentProb = pair.merge((left, right) -> left); + + newResults.add(new Pair<>(currentProb + additionalProbability, list)); + }); + }); + + newResults.forEach((pair) -> { + pair.doWith((left, right) -> { + addCase(ruleName, left, right); + }); + }); + } + + /** + * Create a series of alternatives for a rule by prefixing them with a + * given token + * + * @param additionalProbability + * The amount to adjust the probability by + * @param ruleName + * The name of the rule to prefix + * @param prefixToken + * The token to prefix to the rule + */ + public void prefixRule(final E ruleName, final E prefixToken, final int additionalProbability) { + if (ruleName == null) + throw new NullPointerException("Rule name must not be null"); + else if (prefixToken == null) throw new NullPointerException("Prefix token must not be null"); + + final WeightedRandom<IList<E>> rule = rules.get(ruleName); + + final IList<IPair<Integer, IList<E>>> newResults = new FunctionalList<>(); + + rule.getValues().forEach((pair) -> { + final IList<E> newCase = pair.merge((left, right) -> { + final IList<E> returnVal = new FunctionalList<>(); + + for (final E val : right.toIterable()) { + returnVal.add(val); + } + + return returnVal; + }); + + newCase.prepend(prefixToken); + + newResults.add(new Pair<>(pair.merge((left, right) -> left) + additionalProbability, newCase)); + }); + + newResults.forEach((pair) -> pair.doWith((left, right) -> addCase(ruleName, left, right))); + } + + /** + * Set the initial rule of the graphic + * + * @param initRule + * The initial rule of this grammar + */ + public void setInitialRule(final String initRule) { + this.initialRule = initRule; + } + + /** + * Suffix a token to a rule + * + * @param ruleName + * The rule to suffix + * @param suffixToken + * The token to prefix to the rule + * @param additionalProbability + * Additional probability of the prefixed rule + */ + public void suffixRule(final E ruleName, final E suffixToken, final int additionalProbability) { + if (ruleName == null) + throw new NullPointerException("Rule name must not be null"); + else if (suffixToken == null) throw new NullPointerException("Prefix token must not be null"); + + final WeightedRandom<IList<E>> rule = rules.get(ruleName); + + final IList<IPair<Integer, IList<E>>> newResults = new FunctionalList<>(); + + rule.getValues().forEach((par) -> { + final IList<E> newCase = par.merge((left, right) -> { + final IList<E> returnVal = new FunctionalList<>(); + + for (final E val : right.toIterable()) { + returnVal.add(val); + } + + return returnVal; + }); + + newCase.add(suffixToken); + + newResults.add(new Pair<>(par.merge((left, right) -> left) + additionalProbability, newCase)); + }); + + newResults.forEach((pair) -> pair.doWith((left, right) -> addCase(ruleName, left, right))); + } +} diff --git a/base/src/main/java/bjc/utils/gen/WeightedRandom.java b/base/src/main/java/bjc/utils/gen/WeightedRandom.java new file mode 100644 index 0000000..18225ef --- /dev/null +++ b/base/src/main/java/bjc/utils/gen/WeightedRandom.java @@ -0,0 +1,112 @@ +package bjc.utils.gen; + +import java.util.Random; + +import bjc.utils.data.IHolder; +import bjc.utils.data.IPair; +import bjc.utils.data.Identity; +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.IList; + +/** + * Represents a random number generator where certain results are weighted more + * heavily than others. + * + * @author ben + * + * @param <E> + * The type of values that are randomly selected. + */ +public class WeightedRandom<E> { + /* + * The list of probabilities for each result + */ + private final IList<Integer> probabilities; + + /* + * The list of possible results to pick from + */ + private final IList<E> results; + + /* + * The source for any needed random numbers + */ + private final Random source; + + private int totalChance; + + /** + * Create a new weighted random generator with the specified source of + * randomness + * + * @param src + * The source of randomness to use. + */ + public WeightedRandom(final Random src) { + probabilities = new FunctionalList<>(); + results = new FunctionalList<>(); + + if (src == null) throw new NullPointerException("Source of randomness must not be null"); + + source = src; + } + + /** + * Add a probability for a specific result to be given. + * + * @param chance + * The chance to get this result. + * @param result + * The result to get when the chance comes up. + */ + public void addProbability(final int chance, final E result) { + probabilities.add(chance); + results.add(result); + + totalChance += chance; + } + + /** + * Generate a weighted random value. + * + * @return A random value selected in a weighted fashion. + */ + public E generateValue() { + final IHolder<Integer> value = new Identity<>(source.nextInt(totalChance)); + final IHolder<E> current = new Identity<>(); + final IHolder<Boolean> picked = new Identity<>(true); + + probabilities.forEachIndexed((index, probability) -> { + if (picked.unwrap(bool -> bool)) { + if (value.unwrap((number) -> number < probability)) { + current.transform((result) -> results.getByIndex(index)); + + picked.transform((bool) -> false); + } else { + value.transform((number) -> number - probability); + } + } + }); + + return current.unwrap((result) -> result); + } + + /** + * Return a list of values that can be generated by this generator + * + * @return A list of all the values that can be generated + */ + public IList<E> getResults() { + return results; + } + + /** + * Return a list containing values that can be generated paired with the + * probability of those values being generated + * + * @return A list of pairs of values and value probabilities + */ + public IList<IPair<Integer, E>> getValues() { + return probabilities.pairWith(results); + } +} diff --git a/base/src/main/java/bjc/utils/graph/AdjacencyMap.java b/base/src/main/java/bjc/utils/graph/AdjacencyMap.java new file mode 100644 index 0000000..446ab5b --- /dev/null +++ b/base/src/main/java/bjc/utils/graph/AdjacencyMap.java @@ -0,0 +1,216 @@ +package bjc.utils.graph; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.InputMismatchException; +import java.util.Scanner; + +import bjc.utils.data.IHolder; +import bjc.utils.data.Identity; +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.FunctionalMap; +import bjc.utils.funcdata.IList; +import bjc.utils.funcdata.IMap; +import bjc.utils.funcutils.FuncUtils; + +/** + * An adjacency map representing a graph + * + * @author ben + * + * @param <T> + * The type of the nodes in the graph + */ +public class AdjacencyMap<T> { + /** + * Create an adjacency map from a stream of text + * + * @param stream + * The stream of text to read in + * @return An adjacency map defined by the text + */ + public static AdjacencyMap<Integer> fromStream(final InputStream stream) { + if (stream == null) throw new NullPointerException("Input source must not be null"); + + // Create the adjacency map + AdjacencyMap<Integer> adjacency; + + try (Scanner input = new Scanner(stream)) { + input.useDelimiter("\n"); + + int vertexCount; + + final String possible = input.next(); + + try { + // First, read in number of vertices + vertexCount = Integer.parseInt(possible); + } catch (final NumberFormatException nfex) { + final InputMismatchException imex = new InputMismatchException( + "The first line must contain the number of vertices. " + possible + + " is not a valid number"); + + imex.initCause(nfex); + + throw imex; + } + + if (vertexCount <= 0) + throw new InputMismatchException("The number of vertices must be greater than 0"); + + final IList<Integer> vertices = new FunctionalList<>(); + + FuncUtils.doTimes(vertexCount, (vertexNo) -> vertices.add(vertexNo)); + + adjacency = new AdjacencyMap<>(vertices); + + final IHolder<Integer> row = new Identity<>(0); + + input.forEachRemaining((strang) -> { + readRow(adjacency, vertexCount, row, strang); + }); + } + + return adjacency; + } + + private static void readRow(final AdjacencyMap<Integer> adjacency, final int vertexCount, + final IHolder<Integer> row, final String strang) { + final String[] parts = strang.split(" "); + + if (parts.length != vertexCount) + throw new InputMismatchException("Must specify a weight for all " + vertexCount + " vertices"); + + int column = 0; + + for (final String part : parts) { + int weight; + + try { + weight = Integer.parseInt(part); + } catch (final NumberFormatException nfex) { + final InputMismatchException imex = new InputMismatchException( + "" + part + " is not a valid weight."); + + imex.initCause(nfex); + + throw imex; + } + + adjacency.setWeight(row.getValue(), column, weight); + + column++; + } + + row.transform((rowNumber) -> rowNumber + 1); + } + + /** + * The backing storage of the map + */ + private final IMap<T, IMap<T, Integer>> adjacency = new FunctionalMap<>(); + + /** + * Create a new map from a set of vertices + * + * @param vertices + * The set of vertices to create a map from + */ + public AdjacencyMap(final IList<T> vertices) { + if (vertices == null) throw new NullPointerException("Vertices must not be null"); + + vertices.forEach(vertex -> { + final IMap<T, Integer> row = new FunctionalMap<>(); + + vertices.forEach(target -> { + row.put(target, 0); + }); + + adjacency.put(vertex, row); + }); + } + + /** + * Check if the graph is directed + * + * @return Whether or not the graph is directed + */ + public boolean isDirected() { + final IHolder<Boolean> result = new Identity<>(true); + + adjacency.forEach((sourceKey, sourceValue) -> { + sourceValue.forEach((targetKey, targetValue) -> { + final int inverseValue = adjacency.get(targetKey).get(sourceKey); + + if (targetValue != inverseValue) { + result.replace(false); + } + }); + }); + + return result.getValue(); + } + + /** + * Set the weight of an edge + * + * @param source + * The source node of the edge + * @param target + * The target node of the edge + * @param weight + * The weight of the edge + */ + public void setWeight(final T source, final T target, final int weight) { + if (source == null) + throw new NullPointerException("Source vertex must not be null"); + else if (target == null) throw new NullPointerException("Target vertex must not be null"); + + if (!adjacency.containsKey(source)) + throw new IllegalArgumentException("Source vertex " + source + " isn't present in map"); + else if (!adjacency.containsKey(target)) + throw new IllegalArgumentException("Target vertex " + target + " isn't present in map"); + + adjacency.get(source).put(target, weight); + } + + /** + * Convert this to a different graph representation + * + * @return The new representation of this graph + */ + public Graph<T> toGraph() { + final Graph<T> ret = new Graph<>(); + + adjacency.forEach((sourceKey, sourceValue) -> { + sourceValue.forEach((targetKey, targetValue) -> { + ret.addEdge(sourceKey, targetKey, targetValue, true); + }); + }); + + return ret; + } + + /** + * Convert an adjacency map back into a stream + * + * @param sink + * The stream to convert to + */ + public void toStream(final OutputStream sink) { + if (sink == null) throw new NullPointerException("Output source must not be null"); + + final PrintStream outputPrinter = new PrintStream(sink); + + adjacency.forEach((sourceKey, sourceValue) -> { + sourceValue.forEach((targetKey, targetValue) -> { + outputPrinter.printf("%d", targetValue); + }); + + outputPrinter.println(); + }); + + outputPrinter.close(); + } +} diff --git a/base/src/main/java/bjc/utils/graph/Edge.java b/base/src/main/java/bjc/utils/graph/Edge.java new file mode 100644 index 0000000..0152e3d --- /dev/null +++ b/base/src/main/java/bjc/utils/graph/Edge.java @@ -0,0 +1,112 @@ +package bjc.utils.graph; + +/** + * An edge in a weighted graph + * + * @author ben + * + * @param <T> + * The type of the nodes in the graph + */ +public class Edge<T> { + /* + * The distance from initial to terminal node + */ + private final int distance; + + /* + * The initial and terminal nodes of this edge + */ + private final T source, target; + + /** + * Create a new edge with set parameters + * + * @param initial + * The initial node of the edge + * @param terminal + * The terminal node of the edge + * @param distance + * The distance between initial and terminal edge + */ + public Edge(final T initial, final T terminal, final int distance) { + if (initial == null) + throw new NullPointerException("Initial node must not be null"); + else if (terminal == null) throw new NullPointerException("Terminal node must not be null"); + + this.source = initial; + this.target = terminal; + this.distance = distance; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + else if (obj == null) + return false; + else if (getClass() != obj.getClass()) + return false; + else { + final Edge<?> other = (Edge<?>) obj; + + if (distance != other.distance) + return false; + else if (source == null) { + if (other.source != null) return false; + } else if (!source.equals(other.source)) + return false; + else if (target == null) { + if (other.target != null) return false; + } else if (!target.equals(other.target)) return false; + + return true; + } + } + + /** + * Get the distance in this edge + * + * @return The distance between the initial and terminal nodes of this + * edge + */ + public int getDistance() { + return distance; + } + + /** + * Get the initial node of an edge + * + * @return The initial node of this edge + */ + public T getSource() { + return source; + } + + /** + * Get the target node of an edge + * + * @return The target node of this edge + */ + public T getTarget() { + return target; + } + + @Override + public int hashCode() { + final int prime = 31; + + int result = 1; + + result = prime * result + distance; + result = prime * result + (source == null ? 0 : source.hashCode()); + result = prime * result + (target == null ? 0 : target.hashCode()); + + return result; + } + + @Override + public String toString() { + return " first vertex " + source + " to vertex " + target + " with distance: " + distance; + } +} diff --git a/base/src/main/java/bjc/utils/graph/Graph.java b/base/src/main/java/bjc/utils/graph/Graph.java new file mode 100644 index 0000000..280a7f5 --- /dev/null +++ b/base/src/main/java/bjc/utils/graph/Graph.java @@ -0,0 +1,267 @@ +package bjc.utils.graph; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.PriorityQueue; +import java.util.Queue; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiPredicate; + +import bjc.utils.data.IHolder; +import bjc.utils.data.Identity; +import bjc.utils.funcdata.FunctionalMap; +import bjc.utils.funcdata.IList; +import bjc.utils.funcdata.IMap; + +/** + * A directed weighted graph, where the vertices have some arbitrary label + * + * @author ben + * + * @param <T> + * The label for vertices + */ +public class Graph<T> { + /** + * Create a graph from a list of edges + * + * @param <E> + * The type of data stored in the edges + * + * @param edges + * The list of edges to build from + * @return A graph built from the provided edge-list + */ + public static <E> Graph<E> fromEdgeList(final List<Edge<E>> edges) { + final Graph<E> g = new Graph<>(); + + edges.forEach(edge -> { + g.addEdge(edge.getSource(), edge.getTarget(), edge.getDistance(), true); + }); + + return g; + } + + /** + * The backing representation of the graph + */ + private final IMap<T, IMap<T, Integer>> backing; + + /** + * Create a new graph + */ + public Graph() { + backing = new FunctionalMap<>(); + } + + /** + * Add a edge to the graph + * + * @param source + * The source vertex for this edge + * @param target + * The target vertex for this edge + * @param distance + * The distance from the source vertex to the target + * vertex + * @param directed + * Whether or not + */ + public void addEdge(final T source, final T target, final int distance, final boolean directed) { + // Can't add edges with a null source or target + if (source == null) + throw new NullPointerException("The source vertex cannot be null"); + else if (target == null) throw new NullPointerException("The target vertex cannot be null"); + + // Initialize adjacency list for vertices if necessary + if (!backing.containsKey(source)) { + backing.put(source, new FunctionalMap<T, Integer>()); + } + + // Add the edge to the graph + backing.get(source).put(target, distance); + + // Handle possible directed edges + if (!directed) { + if (!backing.containsKey(target)) { + backing.put(target, new FunctionalMap<T, Integer>()); + } + + backing.get(target).put(source, distance); + } + } + + /** + * Execute an action for all edges of a specific vertex matching + * conditions + * + * @param source + * The vertex to test edges for + * @param matcher + * The conditions an edge must match + * @param action + * The action to execute for matching edges + */ + public void forAllEdgesMatchingAt(final T source, final BiPredicate<T, Integer> matcher, + final BiConsumer<T, Integer> action) { + if (matcher == null) + throw new NullPointerException("Matcher must not be null"); + else if (action == null) throw new NullPointerException("Action must not be null"); + + getEdges(source).forEach((target, weight) -> { + if (matcher.test(target, weight)) { + action.accept(target, weight); + } + }); + } + + /** + * Get all the edges that begin at a particular source vertex + * + * @param source + * The vertex to use as a source + * @return All of the edges with the specified vertex as a source + */ + public IMap<T, Integer> getEdges(final T source) { + // Can't find edges for a null source + if (source == null) + throw new NullPointerException("The source cannot be null."); + else if (!backing.containsKey(source)) + throw new IllegalArgumentException("Vertex " + source + " is not in graph"); + + return backing.get(source); + } + + /** + * Get the initial vertex of the graph + * + * @return The initial vertex of the graph + */ + public T getInitial() { + return backing.keyList().first(); + } + + /** + * Uses Prim's algorothm to calculate a MST for the graph. + * + * If the graph is non-connected, this will lead to unpredictable + * results. + * + * @return a list of edges that constitute the MST + */ + public List<Edge<T>> getMinimumSpanningTree() { + // Set of all of the currently available edges + final Queue<Edge<T>> available = new PriorityQueue<>(10, + (left, right) -> left.getDistance() - right.getDistance()); + + // The MST of the graph + final List<Edge<T>> minimums = new ArrayList<>(); + + // The set of all of the visited vertices. + final Set<T> visited = new HashSet<>(); + + // Start at the initial vertex and visit it + final IHolder<T> source = new Identity<>(getInitial()); + + visited.add(source.getValue()); + + // Make sure we visit all the nodes + while (visited.size() != getVertexCount()) { + // Grab all edges adjacent to the provided edge + + forAllEdgesMatchingAt(source.getValue(), (target, weight) -> { + return !visited.contains(target); + }, (target, weight) -> { + final T vert = source.unwrap(vertex -> vertex); + + available.add(new Edge<>(vert, target, weight)); + }); + + // Get the edge with the minimum distance + final IHolder<Edge<T>> minimum = new Identity<>(available.poll()); + + // Only consider edges where we haven't visited the + // target of + // the edge + while (visited.contains(minimum.getValue().getTarget())) { + minimum.transform((edge) -> available.poll()); + } + + // Add it to our MST + minimums.add(minimum.getValue()); + + // Advance to the next node + source.transform((vertex) -> minimum.unwrap(edge -> edge.getTarget())); + + // Visit this node + visited.add(source.getValue()); + } + + return minimums; + } + + /** + * Get the count of the vertices in this graph + * + * @return A count of the vertices in this graph + */ + public int getVertexCount() { + return backing.size(); + } + + /** + * Get all of the vertices in this graph. + * + * @return A unmodifiable set of all the vertices in the graph. + */ + public IList<T> getVertices() { + return backing.keyList(); + } + + /** + * Remove the edge starting at the source and ending at the target + * + * @param source + * The source vertex for the edge + * @param target + * The target vertex for the edge + */ + public void removeEdge(final T source, final T target) { + // Can't remove things w/ null vertices + if (source == null) + throw new NullPointerException("The source vertex cannot be null"); + else if (target == null) throw new NullPointerException("The target vertex cannot be null"); + + // Can't remove if one vertice doesn't exists + if (!backing.containsKey(source)) + throw new NoSuchElementException("vertex " + source + " does not exist."); + + if (!backing.containsKey(target)) + throw new NoSuchElementException("vertex " + target + " does not exist."); + + backing.get(source).remove(target); + + // Uncomment this to turn the graph undirected + // graph.get(target).remove(source); + } + + /** + * Convert a graph into a adjacency map/matrix + * + * @return A adjacency map representing this graph + */ + public AdjacencyMap<T> toAdjacencyMap() { + final AdjacencyMap<T> adjacency = new AdjacencyMap<>(backing.keyList()); + + backing.forEach((sourceKey, sourceValue) -> { + sourceValue.forEach((targetKey, targetValue) -> { + adjacency.setWeight(sourceKey, targetKey, targetValue); + }); + }); + + return adjacency; + } +} diff --git a/base/src/main/java/bjc/utils/gui/ExtensionFileFilter.java b/base/src/main/java/bjc/utils/gui/ExtensionFileFilter.java new file mode 100644 index 0000000..7c487eb --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/ExtensionFileFilter.java @@ -0,0 +1,56 @@ +package bjc.utils.gui; + +import java.io.File; +import java.util.List; + +import javax.swing.filechooser.FileFilter; + +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.IList; + +/** + * A file filter based on extensions. + * + * Built for Swing. + * + * @author ben + * + */ +public class ExtensionFileFilter extends FileFilter { + /** + * The list holding all filtered extensions + */ + private final IList<String> extensions; + + /** + * Create a new filter only showing files with the specified extensions. + * + * @param exts + * The extensions to show in this filter. + */ + public ExtensionFileFilter(final List<String> exts) { + extensions = new FunctionalList<>(exts); + } + + /** + * Create a new filter only showing files with the specified extensions. + * + * @param exts + * The extensions to show in this filter. + */ + public ExtensionFileFilter(final String... exts) { + extensions = new FunctionalList<>(exts); + } + + @Override + public boolean accept(final File pathname) { + if (pathname == null) throw new NullPointerException("Pathname must not be null"); + + return extensions.anyMatch(pathname.getName()::endsWith); + } + + @Override + public String getDescription() { + return extensions.toString(); + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/gui/SimpleDialogs.java b/base/src/main/java/bjc/utils/gui/SimpleDialogs.java new file mode 100644 index 0000000..59eb1c3 --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/SimpleDialogs.java @@ -0,0 +1,269 @@ +package bjc.utils.gui; + +import java.awt.Component; +import java.awt.Frame; +import java.util.function.Function; +import java.util.function.Predicate; + +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; + +import bjc.utils.gui.layout.VLayout; + +/** + * Utility class for getting simple input from the user. + * + * @author ben + * + */ +public class SimpleDialogs { + /** + * Get a bounded integer from the user. + * + * @param parent + * The parent component for the dialogs. + * @param title + * The title for the dialogs. + * @param prompt + * The prompt to tell the user what to enter. + * @param lowerBound + * The lower integer bound to accept. + * @param upperBound + * The upper integer bound to accept. + * @return A int within the specified bounds. + */ + public static int getBoundedInt(final Component parent, final String title, final String prompt, + final int lowerBound, final int upperBound) { + return getValue(parent, title, prompt, (strang) -> { + try { + final int value = Integer.parseInt(strang); + + return value < upperBound && value > lowerBound; + } catch (final NumberFormatException nfex) { + // We don't care about the specifics of the + // exception, just + // that this value isn't good + return false; + } + }, Integer::parseInt); + } + + /** + * Asks the user to pick an option from a series of choices. + * + * @param <E> + * The type of choices for the user to pick + * + * @param parent + * The parent frame for this dialog + * @param title + * The title of this dialog + * @param question + * The question being asked + * @param choices + * The available choices for the question + * @return The choice the user picked, or null if they didn't pick one + */ + @SuppressWarnings("unchecked") + public static <E> E getChoice(final Frame parent, final String title, final String question, + final E... choices) { + if (parent == null) + throw new NullPointerException("Parent must not be null"); + else if (title == null) + throw new NullPointerException("Title must not be null"); + else if (question == null) throw new NullPointerException("Question must not be null"); + + final JDialog chooser = new JDialog(parent, title, true); + chooser.setLayout(new VLayout(2)); + + final JPanel questionPane = new JPanel(); + + final JLabel questionText = new JLabel(question); + final JComboBox<E> questionChoices = new JComboBox<>(choices); + + questionPane.add(questionText); + questionPane.add(questionChoices); + + final JPanel buttonPane = new JPanel(); + + final JButton okButton = new JButton("Ok"); + final JButton cancelButton = new JButton("Cancel"); + + okButton.addActionListener((event) -> chooser.dispose()); + cancelButton.addActionListener((event) -> chooser.dispose()); + + buttonPane.add(cancelButton); + buttonPane.add(okButton); + + chooser.add(questionPane); + chooser.add(buttonPane); + + chooser.pack(); + chooser.setVisible(true); + + return (E) questionChoices.getSelectedItem(); + } + + /** + * Get a integer from the user + * + * @param parent + * The parent component for dialogs. + * @param title + * The title for dialogs. + * @param prompt + * The prompt to tell the user what to enter. + * @return A int. + */ + public static int getInt(final Component parent, final String title, final String prompt) { + return getValue(parent, title, prompt, strang -> { + try { + Integer.parseInt(strang); + return true; + } catch (final NumberFormatException nfex) { + // We don't care about this exception, just mark + // the value + // as not good + return false; + } + }, Integer::parseInt); + } + + /** + * Get a string from the user + * + * @param parent + * The parent component for dialogs. + * @param title + * The title for the dialogs. + * @param prompt + * The prompt to tell the user what to enter. + * @return A string. + */ + public static String getString(final Component parent, final String title, final String prompt) { + if (parent == null) + throw new NullPointerException("Parent must not be null"); + else if (title == null) + throw new NullPointerException("Title must not be null"); + else if (prompt == null) throw new NullPointerException("Prompt must not be null"); + + return JOptionPane.showInputDialog(parent, prompt, title, JOptionPane.QUESTION_MESSAGE); + } + + /** + * Get a value parsable from a string from the user. + * + * @param <E> + * The type of the value parsed from the string + * + * @param parent + * The parent component for dialogs. + * @param title + * The title for dialogs. + * @param prompt + * The prompt to tell the user what to enter. + * @param validator + * A predicate to determine if a input is valid. + * @param transformer + * The function to transform the string into a value. + * @return The value parsed from a string. + */ + public static <E> E getValue(final Component parent, final String title, final String prompt, + final Predicate<String> validator, final Function<String, E> transformer) { + if (validator == null) + throw new NullPointerException("Validator must not be null"); + else if (transformer == null) throw new NullPointerException("Transformer must not be null"); + + String input = getString(parent, title, prompt); + + while (!validator.test(input)) { + showError(parent, "I/O Error", "Please enter a valid value"); + + input = getString(parent, title, prompt); + } + + return transformer.apply(input); + } + + /** + * Get a whole number from the user. + * + * @param parent + * The parent component for dialogs. + * @param title + * The title for dialogs. + * @param prompt + * The prompt to tell the user what to enter. + * @return A whole number. + */ + public static int getWhole(final Component parent, final String title, final String prompt) { + return getBoundedInt(parent, title, prompt, 0, Integer.MAX_VALUE); + } + + /** + * Ask the user a Yes/No question. + * + * @param parent + * The parent component for dialogs. + * @param title + * The title for dialogs. + * @param question + * The question to ask the user. + * @return True if the user said yes, false otherwise. + */ + public static boolean getYesNo(final Component parent, final String title, final String question) { + if (parent == null) + throw new NullPointerException("Parent must not be null"); + else if (title == null) + throw new NullPointerException("Title must not be null"); + else if (question == null) throw new NullPointerException("Question must not be null"); + + final int result = JOptionPane.showConfirmDialog(parent, question, title, JOptionPane.YES_NO_OPTION); + + return result == JOptionPane.YES_OPTION ? true : false; + } + + /** + * Show a error message to the user + * + * @param parent + * The parent component for dialogs. + * @param title + * The title for dialogs. + * @param message + * The error to show the user. + */ + public static void showError(final Component parent, final String title, final String message) { + if (parent == null) + throw new NullPointerException("Parent must not be null"); + else if (title == null) + throw new NullPointerException("Title must not be null"); + else if (message == null) throw new NullPointerException("Error message must not be null"); + + JOptionPane.showMessageDialog(parent, message, title, JOptionPane.ERROR_MESSAGE); + } + + /** + * Show an informative message to the user + * + * @param parent + * The parent for this dialog + * @param title + * Show the title for this dialog + * @param message + * Show the message for this dialog + */ + public static void showMessage(final Component parent, final String title, final String message) { + if (parent == null) + throw new NullPointerException("Parent must not be null"); + else if (title == null) + throw new NullPointerException("Title must not be null"); + else if (message == null) throw new NullPointerException("Message must not be null"); + + JOptionPane.showMessageDialog(parent, title, message, JOptionPane.INFORMATION_MESSAGE); + } +} diff --git a/base/src/main/java/bjc/utils/gui/SimpleFileChooser.java b/base/src/main/java/bjc/utils/gui/SimpleFileChooser.java new file mode 100644 index 0000000..7da0bd8 --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/SimpleFileChooser.java @@ -0,0 +1,198 @@ +package bjc.utils.gui; + +import java.awt.Component; +import java.io.File; + +import javax.swing.JFileChooser; + +import bjc.utils.exceptions.FileNotChosenException; + +/** + * Utility class for easily prompting user for files. + * + * Built for Swing. + * + * @author ben + * + */ +public class SimpleFileChooser { + private static File doOpenFile(final Component parent, final String title, final JFileChooser files) { + if (title == null) throw new NullPointerException("Title must not be null"); + + files.setDialogTitle(title); + + boolean success = false; + + while (!success) { + try { + maybeDoOpenFile(parent, files); + + success = true; + } catch (final FileNotChosenException fncx) { + // We don't care about specifics + SimpleDialogs.showError(parent, "I/O Error", "Please pick a file to open"); + } + } + + return files.getSelectedFile(); + } + + private static File doSaveFile(final Component parent, final String title, final JFileChooser files) { + if (title == null) throw new NullPointerException("Title must not be null"); + + files.setDialogTitle(title); + + final boolean success = false; + + while (!success) { + try { + maybeDoSaveFile(parent, files); + + return files.getSelectedFile(); + } catch (final FileNotChosenException fncex) { + // We don't care about specifics + SimpleDialogs.showError(parent, "I/O Error", "Please pick a file to save to"); + } + } + } + + /** + * Prompt the user with a "Open File..." dialog. Keeps prompting them + * until they pick a file. + * + * @param parent + * The component to use as the parent for the dialog. + * @param title + * The title of the dialog to prompt with. + * @return The file the user has chosen. + */ + public static File getOpenFile(final Component parent, final String title) { + final JFileChooser files = new JFileChooser(); + + return doOpenFile(parent, title, files); + } + + /** + * Prompt the user with a "Open File..." dialog. Keeps prompting them + * until they pick a file. + * + * @param parent + * The component to use as the parent for the dialog. + * @param title + * The title of the dialog to prompt with. + * @param extensions + * The list of file extensions the file should have. + * @return The file the user has chosen. + */ + public static File getOpenFile(final Component parent, final String title, final String... extensions) { + final JFileChooser files = new JFileChooser(); + + files.addChoosableFileFilter(new ExtensionFileFilter(extensions)); + + return doOpenFile(parent, title, files); + } + + /** + * Prompt the user with a "Save File..." dialog. + * + * @param parent + * The component to use as the parent for the dialog. + * @param title + * The title of the dialog to prompt with. + * @return The file the user chose. + */ + public static File getSaveFile(final Component parent, final String title) { + final JFileChooser files = new JFileChooser(); + + return doSaveFile(parent, title, files); + } + + /** + * Prompt the user with a "Save File..." dialog. + * + * @param parent + * The component to use as the parent for the dialog. + * @param title + * The title of the dialog to prompt with. + * @param extensions + * The extensions of the files the user can choose. + * @return The file the user chose. + */ + public static File getSaveFile(final Component parent, final String title, final String... extensions) { + final JFileChooser files = new JFileChooser(); + + files.addChoosableFileFilter(new ExtensionFileFilter(extensions)); + + return doSaveFile(parent, title, files); + } + + private static void maybeDoOpenFile(final Component parent, final JFileChooser files) + throws FileNotChosenException { + if (parent == null) + throw new NullPointerException("Parent must not be null"); + else if (files == null) throw new NullPointerException("File chooser must not be null"); + + final int result = files.showSaveDialog(parent); + + if (result != JFileChooser.APPROVE_OPTION) throw new FileNotChosenException(); + } + + private static void maybeDoSaveFile(final Component parent, final JFileChooser files) + throws FileNotChosenException { + if (parent == null) + throw new NullPointerException("Parent must not be null"); + else if (files == null) throw new NullPointerException("File chooser must not be null"); + + final int result = files.showSaveDialog(parent); + + if (result != JFileChooser.APPROVE_OPTION) throw new FileNotChosenException(); + } + + /** + * Prompt the user with a "Open File..." dialog. + * + * @param parent + * The component to use as the parent for the dialog. + * @param title + * The title of the dialog to prompt with. + * @return The file if the user chose one or null if they didn't. + */ + public static File maybeOpenFile(final Component parent, final String title) { + if (title == null) throw new NullPointerException("Title must not be null"); + + final JFileChooser files = new JFileChooser(); + files.setDialogTitle(title); + + try { + maybeDoOpenFile(parent, files); + } catch (final FileNotChosenException fncex) { + // We don't care about specifics + } + + return files.getSelectedFile(); + } + + /** + * Prompt the user with a "Save File..." dialog. + * + * @param parent + * The component to use as the parent for the dialog. + * @param title + * The title of the dialog to prompt with. + * @return The file if the user chose one or null if they didn't. + */ + public static File maybeSaveFile(final Component parent, final String title) { + if (title == null) throw new NullPointerException("Title must not be null"); + + final JFileChooser files = new JFileChooser(); + files.setDialogTitle(title); + + try { + maybeDoSaveFile(parent, files); + } catch (final FileNotChosenException fncex) { + // We don't care about specifics + } + + return files.getSelectedFile(); + } +} diff --git a/base/src/main/java/bjc/utils/gui/SimpleInternalDialogs.java b/base/src/main/java/bjc/utils/gui/SimpleInternalDialogs.java new file mode 100644 index 0000000..5237557 --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/SimpleInternalDialogs.java @@ -0,0 +1,208 @@ +package bjc.utils.gui; + +import java.awt.Component; +import java.util.function.Function; +import java.util.function.Predicate; + +import javax.swing.JOptionPane; + +/** + * Utility class for getting simple input from the user. + * + * Modified to work with JDesktopPanes + * + * @author ben + * + */ +public class SimpleInternalDialogs { + /** + * Get a bounded integer from the user. + * + * @param parent + * The parent component for the dialogs. + * @param title + * The title for the dialogs. + * @param prompt + * The prompt to tell the user what to enter. + * @param lowerBound + * The lower integer bound to accept. + * @param upperBound + * The upper integer bound to accept. + * @return A int within the specified bounds. + */ + public static int getBoundedInt(final Component parent, final String title, final String prompt, + final int lowerBound, final int upperBound) { + return getValue(parent, title, prompt, (strang) -> { + try { + final int value = Integer.parseInt(strang); + + return value < upperBound && value > lowerBound; + } catch (final NumberFormatException nfex) { + // We don't care about the specifics of the + // exception, just + // that this value isn't good + return false; + } + }, Integer::parseInt); + } + + /** + * Get a integer from the user + * + * @param parent + * The parent component for dialogs. + * @param title + * The title for dialogs. + * @param prompt + * The prompt to tell the user what to enter. + * @return A int. + */ + public static int getInt(final Component parent, final String title, final String prompt) { + return getValue(parent, title, prompt, strang -> { + try { + Integer.parseInt(strang); + return true; + } catch (final NumberFormatException nfex) { + // We don't care about this exception, just mark + // the value + // as not good + return false; + } + }, Integer::parseInt); + } + + /** + * Get a string from the user + * + * @param parent + * The parent component for dialogs. + * @param title + * The title for the dialogs. + * @param prompt + * The prompt to tell the user what to enter. + * @return A string. + */ + public static String getString(final Component parent, final String title, final String prompt) { + if (parent == null) + throw new NullPointerException("Parent must not be null"); + else if (title == null) + throw new NullPointerException("Title must not be null"); + else if (prompt == null) throw new NullPointerException("Prompt must not be null"); + + return JOptionPane.showInternalInputDialog(parent, prompt, title, JOptionPane.QUESTION_MESSAGE); + } + + /** + * Get a value parsable from a string from the user. + * + * @param <E> + * The type of the value parsed from the string + * + * @param parent + * The parent component for dialogs. + * @param title + * The title for dialogs. + * @param prompt + * The prompt to tell the user what to enter. + * @param validator + * A predicate to determine if a input is valid. + * @param transformer + * The function to transform the string into a value. + * @return The value parsed from a string. + */ + public static <E> E getValue(final Component parent, final String title, final String prompt, + final Predicate<String> validator, final Function<String, E> transformer) { + if (validator == null) + throw new NullPointerException("Validator must not be null"); + else if (transformer == null) throw new NullPointerException("Transformer must not be null"); + + String strang = getString(parent, title, prompt); + + while (!validator.test(strang)) { + showError(parent, "I/O Error", "Please enter a valid value"); + + strang = getString(parent, title, prompt); + } + + return transformer.apply(strang); + } + + /** + * Get a whole number from the user. + * + * @param parent + * The parent component for dialogs. + * @param title + * The title for dialogs. + * @param prompt + * The prompt to tell the user what to enter. + * @return A whole number. + */ + public static int getWhole(final Component parent, final String title, final String prompt) { + return getBoundedInt(parent, title, prompt, 0, Integer.MAX_VALUE); + } + + /** + * Ask the user a Yes/No question. + * + * @param parent + * The parent component for dialogs. + * @param title + * The title for dialogs. + * @param question + * The question to ask the user. + * @return True if the user said yes, false otherwise. + */ + public static boolean getYesNo(final Component parent, final String title, final String question) { + if (parent == null) + throw new NullPointerException("Parent must not be null"); + else if (title == null) + throw new NullPointerException("Title must not be null"); + else if (question == null) throw new NullPointerException("Question must not be null"); + + final int result = JOptionPane.showInternalConfirmDialog(parent, question, title, + JOptionPane.YES_NO_OPTION); + + return result == JOptionPane.YES_OPTION ? true : false; + } + + /** + * Show a error message to the user + * + * @param parent + * The parent component for dialogs. + * @param title + * The title for dialogs. + * @param message + * The error to show the user. + */ + public static void showError(final Component parent, final String title, final String message) { + if (parent == null) + throw new NullPointerException("Parent must not be null"); + else if (title == null) + throw new NullPointerException("Title must not be null"); + else if (message == null) throw new NullPointerException("Error message must not be null"); + + JOptionPane.showInternalMessageDialog(parent, message, title, JOptionPane.ERROR_MESSAGE); + } + + /** + * Show an informative message to the user + * + * @param parent + * The parent for this dialog + * @param title + * Show the title for this dialog + * @param message + * Show the message for this dialog + */ + public static void showMessage(final Component parent, final String title, final String message) { + if (parent == null) + throw new NullPointerException("Parent must not be null"); + else if (title == null) + throw new NullPointerException("Title must not be null"); + else if (message == null) throw new NullPointerException("Message must not be null"); + + JOptionPane.showInternalMessageDialog(parent, title, message, JOptionPane.INFORMATION_MESSAGE); + } +} diff --git a/base/src/main/java/bjc/utils/gui/SimpleInternalFrame.java b/base/src/main/java/bjc/utils/gui/SimpleInternalFrame.java new file mode 100644 index 0000000..afb498e --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/SimpleInternalFrame.java @@ -0,0 +1,40 @@ +package bjc.utils.gui; + +import javax.swing.JInternalFrame; + +/** + * A simple internal frame class + * + * @author ben + * + */ +public class SimpleInternalFrame extends JInternalFrame { + private static final long serialVersionUID = -2966801321260716617L; + + /** + * Create a new blank internal frame + */ + public SimpleInternalFrame() { + super(); + } + + /** + * Create a new blank internal frame with a specific title + * + * @param title + * The title of the internal frame + */ + public SimpleInternalFrame(final String title) { + super(title); + } + + protected void setupFrame() { + setSize(320, 240); + + setResizable(true); + + setClosable(true); + setMaximizable(true); + setIconifiable(true); + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/gui/SimpleJList.java b/base/src/main/java/bjc/utils/gui/SimpleJList.java new file mode 100644 index 0000000..411d0db --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/SimpleJList.java @@ -0,0 +1,49 @@ +package bjc.utils.gui; + +import javax.swing.DefaultListModel; +import javax.swing.JList; +import javax.swing.ListModel; + +/** + * Utility class for making JLists and their models. + * + * @author ben + * + */ +public class SimpleJList { + /** + * Create a new JList from a given list. + * + * @param <E> + * The type of data in the JList + * + * @param source + * The list to populate the JList with. + * @return A JList populated with the elements from ls. + */ + public static <E> JList<E> buildFromList(final Iterable<E> source) { + if (source == null) throw new NullPointerException("Source must not be null"); + + return new JList<>(buildModel(source)); + } + + /** + * Create a new list model from a given list. + * + * @param <E> + * The type of data in the list model + * + * @param source + * The list to fill the list model from. + * @return A list model populated with the elements from ls. + */ + public static <E> ListModel<E> buildModel(final Iterable<E> source) { + if (source == null) throw new NullPointerException("Source must not be null"); + + final DefaultListModel<E> defaultModel = new DefaultListModel<>(); + + source.forEach(defaultModel::addElement); + + return defaultModel; + } +} diff --git a/base/src/main/java/bjc/utils/gui/SimpleTitledBorder.java b/base/src/main/java/bjc/utils/gui/SimpleTitledBorder.java new file mode 100644 index 0000000..9b01507 --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/SimpleTitledBorder.java @@ -0,0 +1,25 @@ +package bjc.utils.gui; + +import javax.swing.border.EtchedBorder; +import javax.swing.border.TitledBorder; + +/** + * A simple border with a title attached to it. + * + * @author ben + * + */ +public class SimpleTitledBorder extends TitledBorder { + // Version ID for serialization + private static final long serialVersionUID = -5655969079949148487L; + + /** + * Create a new border with the specified title. + * + * @param title + * The title for the border. + */ + public SimpleTitledBorder(final String title) { + super(new EtchedBorder(), title); + } +} diff --git a/base/src/main/java/bjc/utils/gui/TextAreaOutputStream.java b/base/src/main/java/bjc/utils/gui/TextAreaOutputStream.java new file mode 100644 index 0000000..fbc58ed --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/TextAreaOutputStream.java @@ -0,0 +1,35 @@ +package bjc.utils.gui; + +import java.io.IOException; +import java.io.OutputStream; + +import javax.swing.JTextArea; + +/** + * An output stream that prints to a JTextArea + * + * @author epr + * @author Levente S\u00e1ntha (lsantha@users.sourceforge.net) + */ +public class TextAreaOutputStream extends OutputStream { + private final JTextArea textArea; + + /** + * Create a new output stream attached to a textarea + * + * @param console + * The textarea to write to + */ + public TextAreaOutputStream(final JTextArea console) { + this.textArea = console; + } + + @Override + public void write(final int b) throws IOException { + textArea.append("" + (char) b); + + if (b == '\n') { + textArea.repaint(); + } + } +} diff --git a/base/src/main/java/bjc/utils/gui/awt/ExtensionFileFilter.java b/base/src/main/java/bjc/utils/gui/awt/ExtensionFileFilter.java new file mode 100644 index 0000000..eb60ae2 --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/awt/ExtensionFileFilter.java @@ -0,0 +1,50 @@ +package bjc.utils.gui.awt; + +import java.io.File; +import java.io.FilenameFilter; +import java.util.List; + +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.IList; + +/** + * Filter a set of filenames by extension. + * + * Built for AWT + * + * @author ben + * + */ +public class ExtensionFileFilter implements FilenameFilter { + /** + * The list of extensions to filter + */ + private final IList<String> extensions; + + /** + * Create a new filter only showing files with the specified extensions. + * + * @param exts + * The extensions to show in this filter. + */ + public ExtensionFileFilter(final List<String> exts) { + if (exts == null) throw new NullPointerException("Extensions must not be null"); + + extensions = new FunctionalList<>(exts); + } + + /** + * Create a new filter only showing files with the specified extensions. + * + * @param exts + * The extensions to show in this filter. + */ + public ExtensionFileFilter(final String... exts) { + extensions = new FunctionalList<>(exts); + } + + @Override + public boolean accept(final File directory, final String name) { + return extensions.anyMatch(name::endsWith); + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/gui/awt/SimpleFileDialog.java b/base/src/main/java/bjc/utils/gui/awt/SimpleFileDialog.java new file mode 100644 index 0000000..77a4a59 --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/awt/SimpleFileDialog.java @@ -0,0 +1,144 @@ +package bjc.utils.gui.awt; + +import java.awt.FileDialog; +import java.awt.Frame; +import java.io.File; +import java.io.FilenameFilter; + +import bjc.utils.gui.SimpleDialogs; + +/** + * A simple way to get the user to pick a file + * + * Built for AWT. + * + * @author ben + * + */ +public class SimpleFileDialog { + /** + * Prompt the user to pick a file to open + * + * @param parent + * The parent of the file picker + * @param title + * The title of the file picker + * @return The file the user picked + */ + public static File getOpenFile(final Frame parent, final String title) { + return getOpenFile(parent, title, (String[]) null); + } + + /** + * Prompt the user to pick a file to open + * + * @param parent + * The parent of the file picker + * @param title + * The title of the file picker + * @param extensions + * The extensions to accept as valid + * @return The file the user picked + */ + public static File getOpenFile(final Frame parent, final String title, final String... extensions) { + if (parent == null) + throw new NullPointerException("Parent must not be null"); + else if (title == null) throw new NullPointerException("Title must not be null"); + + final FileDialog chooser = new FileDialog(parent, title, FileDialog.LOAD); + + if (extensions != null) { + final FilenameFilter filter = new ExtensionFileFilter(extensions); + chooser.setFilenameFilter(filter); + } + + chooser.setVisible(true); + + while (chooser.getFile() == null) { + SimpleDialogs.showError(parent, "File I/O Error", "Please choose a file to open."); + chooser.setVisible(true); + } + + return chooser.getFiles()[0]; + } + + /** + * Prompt the user to pick a file to open + * + * @param parent + * The parent of the file picker + * @param title + * The title of the file picker + * @param extensions + * The extensions to accept as valid + * @return The file the user picked + */ + public static File[] getOpenFiles(final Frame parent, final String title, final String... extensions) { + if (parent == null) + throw new NullPointerException("Parent must not be null"); + else if (title == null) throw new NullPointerException("Title must not be null"); + + final FileDialog chooser = new FileDialog(parent, title, FileDialog.LOAD); + + if (extensions != null) { + final FilenameFilter filter = new ExtensionFileFilter(extensions); + chooser.setFilenameFilter(filter); + } + + chooser.setMultipleMode(true); + chooser.setVisible(true); + + while (chooser.getFile() == null) { + SimpleDialogs.showError(parent, "File I/O Error", "Please choose a file to open."); + chooser.setVisible(true); + } + + return chooser.getFiles(); + } + + /** + * Prompt the user to pick a file to save + * + * @param parent + * The parent of the file picker + * @param title + * The title of the file picker + * @return The file the user picked + */ + public static File getSaveFile(final Frame parent, final String title) { + return getSaveFile(parent, title, (String[]) null); + } + + /** + * Prompt the user to pick a file to save + * + * @param parent + * The parent of the file picker + * @param title + * The title of the file picker + * @param extensions + * The extensions to accept as valid + * @return The file the user picked + */ + public static File getSaveFile(final Frame parent, final String title, final String... extensions) { + if (parent == null) + throw new NullPointerException("Parent must not be null"); + else if (title == null) throw new NullPointerException("Title must not be null"); + + final FileDialog chooser = new FileDialog(parent, title, FileDialog.SAVE); + + if (extensions != null) { + final FilenameFilter filter = new ExtensionFileFilter(extensions); + chooser.setFilenameFilter(filter); + } + + chooser.setVisible(true); + + while (chooser.getFile() == null) { + SimpleDialogs.showError(parent, "File I/O Error", "Please choose a file to save to."); + chooser.setVisible(true); + } + + return chooser.getFiles()[0]; + } +} diff --git a/base/src/main/java/bjc/utils/gui/layout/AutosizeLayout.java b/base/src/main/java/bjc/utils/gui/layout/AutosizeLayout.java new file mode 100644 index 0000000..6f384f2 --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/layout/AutosizeLayout.java @@ -0,0 +1,22 @@ +package bjc.utils.gui.layout; + +import java.awt.GridLayout; + +/** + * A layout that simply holds one component that it auto-resizes whenever it is + * resized. + * + * @author ben + * + */ +public class AutosizeLayout extends GridLayout { + // Version id for serialization + private static final long serialVersionUID = -2495693595953396924L; + + /** + * Create a new auto-size layout. + */ + public AutosizeLayout() { + super(1, 1); + } +} diff --git a/base/src/main/java/bjc/utils/gui/layout/HLayout.java b/base/src/main/java/bjc/utils/gui/layout/HLayout.java new file mode 100644 index 0000000..4ed1661 --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/layout/HLayout.java @@ -0,0 +1,25 @@ +package bjc.utils.gui.layout; + +import java.awt.GridLayout; + +/** + * A layout manager that lays out its components horizontally, evenly sizing + * them. + * + * @author ben + * + */ +public class HLayout extends GridLayout { + // Version ID for serialization + private static final long serialVersionUID = 1244964456966270026L; + + /** + * Create a new horizontal layout with the specified number of columns. + * + * @param columns + * The number of columns in this layout. + */ + public HLayout(final int columns) { + super(1, columns); + } +} diff --git a/base/src/main/java/bjc/utils/gui/layout/VLayout.java b/base/src/main/java/bjc/utils/gui/layout/VLayout.java new file mode 100644 index 0000000..6993365 --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/layout/VLayout.java @@ -0,0 +1,25 @@ +package bjc.utils.gui.layout; + +import java.awt.GridLayout; + +/** + * A layout that lays out its components vertically, evenly sharing space among + * them. + * + * @author ben + * + */ +public class VLayout extends GridLayout { + // Version ID for serializations + private static final long serialVersionUID = -6417962941602322663L; + + /** + * Create a new vertical layout with the specified number of rows. + * + * @param rows + * The number of rows. + */ + public VLayout(final int rows) { + super(rows, 1); + } +} diff --git a/base/src/main/java/bjc/utils/gui/panels/DropdownListPanel.java b/base/src/main/java/bjc/utils/gui/panels/DropdownListPanel.java new file mode 100644 index 0000000..4f71d38 --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/panels/DropdownListPanel.java @@ -0,0 +1,73 @@ +package bjc.utils.gui.panels; + +import java.awt.BorderLayout; + +import javax.swing.DefaultListModel; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.ListSelectionModel; + +import bjc.utils.funcdata.IList; +import bjc.utils.gui.layout.AutosizeLayout; +import bjc.utils.gui.layout.HLayout; + +/** + * A panel that allows you to select choices from a dropdown list + * + * @author ben + * + */ +public class DropdownListPanel extends JPanel { + private static final long serialVersionUID = 2719963952350133541L; + + /** + * Create a new dropdown list panel + * + * @param <T> + * The type of items in the dropdown list + * @param type + * The label of the type of items in the list + * @param model + * The model to put items into + * @param choices + * The items to choose from + */ + public <T> DropdownListPanel(final String type, final DefaultListModel<T> model, final IList<T> choices) { + setLayout(new AutosizeLayout()); + + final JPanel itemInputPanel = new JPanel(); + itemInputPanel.setLayout(new BorderLayout()); + + final JPanel addItemPanel = new JPanel(); + addItemPanel.setLayout(new HLayout(2)); + + final JComboBox<T> addItemBox = new JComboBox<>(); + choices.forEach(addItemBox::addItem); + + final JButton addItemButton = new JButton("Add " + type); + + addItemPanel.add(addItemBox); + addItemPanel.add(addItemButton); + + final JList<T> itemList = new JList<>(model); + itemList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + final JButton removeItemButton = new JButton("Remove " + type); + + addItemButton.addActionListener((ev) -> { + model.addElement(addItemBox.getItemAt(addItemBox.getSelectedIndex())); + }); + + removeItemButton.addActionListener((ev) -> { + model.remove(itemList.getSelectedIndex()); + }); + + itemInputPanel.add(addItemPanel, BorderLayout.PAGE_START); + itemInputPanel.add(itemList, BorderLayout.CENTER); + itemInputPanel.add(removeItemButton, BorderLayout.PAGE_END); + + add(itemInputPanel); + } +} diff --git a/base/src/main/java/bjc/utils/gui/panels/FormattedInputPanel.java b/base/src/main/java/bjc/utils/gui/panels/FormattedInputPanel.java new file mode 100644 index 0000000..2cecf0c --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/panels/FormattedInputPanel.java @@ -0,0 +1,66 @@ +package bjc.utils.gui.panels; + +import java.util.function.Consumer; + +import javax.swing.JFormattedTextField; +import javax.swing.JFormattedTextField.AbstractFormatter; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import bjc.utils.gui.layout.HLayout; + +/** + * A simple panel allowing for input of a single formatted value + * + * @author ben + * + * @param <InputVal> + * The type of value being formatted + */ +public class FormattedInputPanel<InputVal> extends JPanel { + private static final long serialVersionUID = 5232016563558588031L; + + private final JFormattedTextField field; + + /** + * Create a new formatted input panel + * + * @param label + * The label for this panel + * @param length + * The length of this panel + * @param formatter + * The formatter to use for input + * @param reciever + * The action to call whenever the value changes + */ + @SuppressWarnings("unchecked") + public FormattedInputPanel(final String label, final int length, final AbstractFormatter formatter, + final Consumer<InputVal> reciever) { + setLayout(new HLayout(2)); + + final JLabel lab = new JLabel(label); + field = new JFormattedTextField(formatter); + + field.setColumns(length); + field.setFocusLostBehavior(JFormattedTextField.COMMIT_OR_REVERT); + field.addPropertyChangeListener("value", (event) -> { + // This is safe, because InputVal should be the type of + // whatever object the formatter is returning + reciever.accept((InputVal) field.getValue()); + }); + + add(lab); + add(field); + } + + /** + * Reset the value in this panel to a specified value + * + * @param value + * The value to set the panel to + */ + public void resetValues(final InputVal value) { + field.setValue(value); + } +} diff --git a/base/src/main/java/bjc/utils/gui/panels/HolderOutputPanel.java b/base/src/main/java/bjc/utils/gui/panels/HolderOutputPanel.java new file mode 100644 index 0000000..653dace --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/panels/HolderOutputPanel.java @@ -0,0 +1,79 @@ +package bjc.utils.gui.panels; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.Timer; + +import bjc.utils.data.IHolder; +import bjc.utils.gui.layout.HLayout; + +/** + * A panel that outputs a value bound to a {@link IHolder} + * + * @author ben + * + */ +public class HolderOutputPanel extends JPanel { + private static final long serialVersionUID = 166573313903782080L; + + private Timer updater; + private final JLabel value; + private final int nDelay; + private final IHolder<String> val; + + /** + * Create a new display panel, backed by a holder + * + * @param lab + * The label to attach to this field + * @param valueHolder + * The holder to get the value from + * @param nDelay + * The delay in ms between value updates + */ + public HolderOutputPanel(final String lab, final IHolder<String> valueHolder, final int nDelay) { + this.val = valueHolder; + this.nDelay = nDelay; + + setLayout(new HLayout(2)); + + final JLabel label = new JLabel(lab); + value = new JLabel("(stopped)"); + + updater = new Timer(nDelay, (event) -> { + value.setText(valueHolder.getValue()); + }); + + add(label); + add(value); + } + + /** + * Set this panel back to its initial state + */ + public void reset() { + stopUpdating(); + + value.setText("(stopped)"); + + updater = new Timer(nDelay, (event) -> { + value.setText(val.getValue()); + }); + } + + /** + * Start updating the contents of the field from the holder + */ + public void startUpdating() { + updater.start(); + } + + /** + * Stop updating the contents of the field from the holder + */ + public void stopUpdating() { + updater.stop(); + + value.setText(value.getText() + " (stopped)"); + } +} diff --git a/base/src/main/java/bjc/utils/gui/panels/ListParameterPanel.java b/base/src/main/java/bjc/utils/gui/panels/ListParameterPanel.java new file mode 100644 index 0000000..cca73d5 --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/panels/ListParameterPanel.java @@ -0,0 +1,133 @@ +package bjc.utils.gui.panels; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +import javax.swing.DefaultListModel; +import javax.swing.JButton; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.ListSelectionModel; + +import bjc.utils.funcdata.IList; +import bjc.utils.gui.SimpleJList; +import bjc.utils.gui.layout.HLayout; +import bjc.utils.gui.layout.VLayout; + +/** + * A panel that has a list of objects and ways of manipulating that list + * + * @author ben + * + * @param <E> + * The type of data stored in the list + */ +public class ListParameterPanel<E> extends JPanel { + // Version id for serialization + private static final long serialVersionUID = 3442971104975491571L; + + /** + * Create a new panel using the specified actions for doing things + * + * @param add + * The action that provides items + * @param edit + * The action that edits items + * @param remove + * The action that removes items + */ + public ListParameterPanel(final Supplier<E> add, final Consumer<E> edit, final Consumer<E> remove) { + this(add, edit, remove, null); + } + + /** + * Create a new panel using the specified actions for doing things + * + * @param add + * The action that provides items + * @param edit + * The action that edits items + * @param remove + * The action that removes items + * @param defaults + * The default values to put in the list + */ + public ListParameterPanel(final Supplier<E> add, final Consumer<E> edit, final Consumer<E> remove, + final IList<E> defaults) { + setLayout(new VLayout(2)); + + JList<E> list; + + if (defaults != null) { + list = SimpleJList.buildFromList(defaults.toIterable()); + } else { + list = new JList<>(new DefaultListModel<>()); + } + + list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + final JPanel buttonPanel = new JPanel(); + + int numButtons = 0; + + if (add != null) { + numButtons++; + } + + if (edit != null) { + numButtons++; + } + + if (remove != null) { + numButtons++; + } + + buttonPanel.setLayout(new HLayout(numButtons)); + + JButton addParam = null; + + if (add != null) { + addParam = new JButton("Add..."); + addParam.addActionListener((event) -> { + final DefaultListModel<E> model = (DefaultListModel<E>) list.getModel(); + + model.addElement(add.get()); + }); + } + + JButton editParam = null; + + if (edit != null) { + editParam = new JButton("Edit..."); + editParam.addActionListener((event) -> { + edit.accept(list.getSelectedValue()); + }); + } + + JButton removeParam = null; + + if (remove != null) { + removeParam = new JButton("Remove..."); + removeParam.addActionListener((event) -> { + final DefaultListModel<E> model = (DefaultListModel<E>) list.getModel(); + + remove.accept(model.remove(list.getSelectedIndex())); + }); + } + + if (add != null) { + buttonPanel.add(addParam); + } + + if (edit != null) { + buttonPanel.add(editParam); + } + + if (remove != null) { + buttonPanel.add(removeParam); + } + + add(list); + add(buttonPanel); + } +} diff --git a/base/src/main/java/bjc/utils/gui/panels/SimpleInputPanel.java b/base/src/main/java/bjc/utils/gui/panels/SimpleInputPanel.java new file mode 100644 index 0000000..65c533d --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/panels/SimpleInputPanel.java @@ -0,0 +1,45 @@ +package bjc.utils.gui.panels; + +import java.awt.BorderLayout; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; + +/** + * A simple component for text input + * + * @author ben + * + */ +public class SimpleInputPanel extends JPanel { + private static final long serialVersionUID = -4734279623645236868L; + + /** + * The text field containing the input value + */ + public final JTextField inputValue; + + /** + * Create a new input panel + * + * @param label + * The label for the field + * @param columns + * The number of columns of text input to take + */ + public SimpleInputPanel(final String label, final int columns) { + setLayout(new BorderLayout()); + + final JLabel inputLabel = new JLabel(label); + + if (columns < 1) { + inputValue = new JTextField(); + } else { + inputValue = new JTextField(columns); + } + + add(inputLabel, BorderLayout.LINE_START); + add(inputValue, BorderLayout.CENTER); + } +} diff --git a/base/src/main/java/bjc/utils/gui/panels/SimpleListPanel.java b/base/src/main/java/bjc/utils/gui/panels/SimpleListPanel.java new file mode 100644 index 0000000..edc1797 --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/panels/SimpleListPanel.java @@ -0,0 +1,93 @@ +package bjc.utils.gui.panels; + +import java.awt.BorderLayout; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import javax.swing.DefaultListModel; +import javax.swing.JButton; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.ListSelectionModel; + +import bjc.utils.gui.layout.AutosizeLayout; +import bjc.utils.gui.layout.HLayout; + +/** + * A simple list of strings + * + * @author ben + * + */ +public class SimpleListPanel extends JPanel { + private static final long serialVersionUID = 2719963952350133541L; + + private static void addItem(final DefaultListModel<String> model, final Predicate<String> verifier, + final Consumer<String> onFailure, final JTextField addItemField) { + final String potentialItem = addItemField.getText(); + + if (verifier == null || verifier.test(potentialItem)) { + model.addElement(potentialItem); + } else { + onFailure.accept(potentialItem); + } + + addItemField.setText(""); + } + + /** + * Create a new list panel + * + * @param type + * The type of things in the list + * @param model + * The model to put items into + * @param verifier + * The predicate to use to verify items + * @param onFailure + * The function to call when an item doesn't verify + */ + public SimpleListPanel(final String type, final DefaultListModel<String> model, + final Predicate<String> verifier, final Consumer<String> onFailure) { + setLayout(new AutosizeLayout()); + + final JPanel itemInputPanel = new JPanel(); + itemInputPanel.setLayout(new BorderLayout()); + + final JPanel addItemPanel = new JPanel(); + addItemPanel.setLayout(new HLayout(2)); + + final JTextField addItemField = new JTextField(255); + final JButton addItemButton = new JButton("Add " + type); + + addItemPanel.add(addItemField); + addItemPanel.add(addItemButton); + + final JList<String> itemList = new JList<>(model); + itemList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + final JScrollPane listScroller = new JScrollPane(itemList); + + final JButton removeItemButton = new JButton("Remove " + type); + + addItemButton.addActionListener((ev) -> { + addItem(model, verifier, onFailure, addItemField); + }); + + addItemField.addActionListener((ev) -> { + addItem(model, verifier, onFailure, addItemField); + }); + + removeItemButton.addActionListener((ev) -> { + model.remove(itemList.getSelectedIndex()); + }); + + itemInputPanel.add(addItemPanel, BorderLayout.PAGE_START); + itemInputPanel.add(listScroller, BorderLayout.CENTER); + itemInputPanel.add(removeItemButton, BorderLayout.PAGE_END); + + add(itemInputPanel); + } +} diff --git a/base/src/main/java/bjc/utils/gui/panels/SimpleSpinnerPanel.java b/base/src/main/java/bjc/utils/gui/panels/SimpleSpinnerPanel.java new file mode 100644 index 0000000..6106182 --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/panels/SimpleSpinnerPanel.java @@ -0,0 +1,42 @@ +package bjc.utils.gui.panels; + +import java.awt.BorderLayout; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SpinnerModel; + +/** + * A simple spinner control + * + * @author ben + * + */ +public class SimpleSpinnerPanel extends JPanel { + private static final long serialVersionUID = -4734279623645236868L; + + /** + * The spinner being used + */ + public final JSpinner inputValue; + + /** + * Create a new spinner panel + * + * @param label + * The label for the spinner + * @param model + * The model to attach to the spinner + */ + public SimpleSpinnerPanel(final String label, final SpinnerModel model) { + setLayout(new BorderLayout()); + + final JLabel inputLabel = new JLabel(label); + + inputValue = new JSpinner(model); + + add(inputLabel, BorderLayout.LINE_START); + add(inputValue, BorderLayout.CENTER); + } +} diff --git a/base/src/main/java/bjc/utils/gui/panels/SliderInputPanel.java b/base/src/main/java/bjc/utils/gui/panels/SliderInputPanel.java new file mode 100644 index 0000000..e6a6da4 --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/panels/SliderInputPanel.java @@ -0,0 +1,187 @@ +package bjc.utils.gui.panels; + +import java.text.ParseException; +import java.util.function.Consumer; + +import javax.swing.JFormattedTextField; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSlider; + +import bjc.utils.gui.layout.HLayout; + +/** + * A simple input panel for a slider-controlled value and a manual-input field + * for setting the slider + * + * @author ben + * + */ +public class SliderInputPanel extends JPanel { + private final class NumberFormatter extends JFormattedTextField.AbstractFormatter { + private static final long serialVersionUID = -4448291795913908270L; + + private final int minValue; + private final int maxValue; + + private final int initValue; + + public NumberFormatter(final SliderSettings settings) { + minValue = settings.minValue; + maxValue = settings.maxValue; + + initValue = settings.initValue; + } + + @Override + public Object stringToValue(final String text) throws ParseException { + try { + final int val = Integer.parseInt(text); + + if (val < minValue) + throw new ParseException("Value must be greater than " + minValue, 0); + else if (val > maxValue) + throw new ParseException("Value must be smaller than " + maxValue, 0); + else return val; + } catch (final NumberFormatException nfex) { + final ParseException pex = new ParseException("Value must be a valid integer", 0); + + pex.initCause(nfex); + + throw pex; + } + } + + @Override + public String valueToString(final Object value) throws ParseException { + if (value == null) return Integer.toString(initValue); + + return Integer.toString((Integer) value); + } + } + + /** + * Represents the settings for a slider + * + * @author ben + * + */ + public static class SliderSettings { + /** + * The minimum value of the slider + */ + public final int minValue; + /** + * The maximum value of the slider + */ + public final int maxValue; + + /** + * The initial value of the slider + */ + public final int initValue; + + /** + * Create a new slider settings, with the initial value in the + * middle + * + * @param min + * The minimum value of the slider + * @param max + * The maximum value of the slider + */ + public SliderSettings(final int min, final int max) { + this(min, max, (min + max) / 2); + } + + /** + * Create a new set of slider sttings + * + * @param min + * The minimum slider value + * @param max + * The maximum slider value + * @param init + * Th initial slider value + */ + public SliderSettings(final int min, final int max, final int init) { + minValue = min; + maxValue = max; + + initValue = init; + } + } + + private static final long serialVersionUID = 2956394160569961404L; + private final JSlider slider; + private final JFormattedTextField field; + + /** + * Create a new slider input panel + * + * @param lab + * The label for the field + * @param settings + * The settings for slider values + * @param majorTick + * The setting for where to place big ticks + * @param minorTick + * The setting for where to place small ticks + * @param action + * The action to execute for a given value + */ + public SliderInputPanel(final String lab, final SliderSettings settings, final int majorTick, + final int minorTick, final Consumer<Integer> action) { + setLayout(new HLayout(3)); + + final JLabel label = new JLabel(lab); + + slider = new JSlider(settings.minValue, settings.maxValue, settings.initValue); + field = new JFormattedTextField(new NumberFormatter(settings)); + + slider.setMajorTickSpacing(majorTick); + slider.setMinorTickSpacing(minorTick); + slider.setPaintTicks(true); + slider.setPaintLabels(true); + + slider.addChangeListener((event) -> { + if (slider.getValueIsAdjusting()) { + // Do nothing + } else { + final int val = slider.getValue(); + + field.setValue(val); + + action.accept(val); + } + }); + + field.setFocusLostBehavior(JFormattedTextField.COMMIT_OR_REVERT); + field.setColumns(15); + field.addPropertyChangeListener("value", (event) -> { + final Object value = field.getValue(); + + if (value == null) { + // Do nothing + } else { + slider.setValue((Integer) value); + } + }); + + add(label); + add(slider); + add(field); + } + + /** + * Reset the values in this panel to a specified value + * + * @param value + * The value to reset the fields to + */ + public void resetValues(final int value) { + slider.setValue(value); + + field.setValue(value); + } +} diff --git a/base/src/main/java/bjc/utils/gui/panels/package-info.java b/base/src/main/java/bjc/utils/gui/panels/package-info.java new file mode 100644 index 0000000..4361885 --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/panels/package-info.java @@ -0,0 +1,5 @@ +/** + * @author ben + * + */ +package bjc.utils.gui.panels;
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/ioutils/CLFormatter.java b/base/src/main/java/bjc/utils/ioutils/CLFormatter.java new file mode 100644 index 0000000..eefd532 --- /dev/null +++ b/base/src/main/java/bjc/utils/ioutils/CLFormatter.java @@ -0,0 +1,531 @@ +package bjc.utils.ioutils;
+
+import java.util.HashMap;
+import java.util.IllegalFormatConversionException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.UnknownFormatConversionException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import bjc.utils.PropertyDB;
+import bjc.utils.esodata.Tape;
+import bjc.utils.esodata.SingleTape;
+
+import static bjc.utils.PropertyDB.applyFormat;
+import static bjc.utils.PropertyDB.getCompiledRegex;
+import static bjc.utils.PropertyDB.getRegex;
+
+public class CLFormatter {
+ public static class CLModifiers {
+ public final boolean atMod;
+ public final boolean colonMod;
+
+ public CLModifiers(boolean at, boolean colon) {
+ atMod = at;
+ colonMod = colon;
+ }
+
+ public static CLModifiers fromString(String modString) {
+ boolean atMod = false;
+ boolean colonMod = false;
+ if(modString != null) {
+ atMod = modString.contains("@");
+ colonMod = modString.contains(":");
+ }
+
+ return new CLModifiers(atMod, colonMod);
+ }
+ }
+
+ public static class EscapeException extends RuntimeException {
+ public final boolean endIteration;
+
+ public EscapeException() {
+ endIteration = false;
+ }
+
+ public EscapeException(boolean end) {
+ endIteration = end;
+ }
+ }
+
+ @FunctionalInterface
+ public interface Directive {
+ /*
+ * @TODO fill in parameters
+ */
+ public void format();
+ }
+
+ private static final String prefixParam = getRegex("clFormatPrefix");
+ private static final Pattern pPrefixParam = Pattern.compile(prefixParam);
+
+ private static final String formatMod = getRegex("clFormatModifier");
+
+ private static final String prefixList = applyFormat("delimSeparatedList", prefixParam, ",");
+
+ private static final String directiveName = getRegex("clFormatName");
+
+ private static final String formatDirective = applyFormat("clFormatDirective", prefixList, formatMod, directiveName);
+ private static final Pattern pFormatDirective = Pattern.compile(formatDirective);
+
+ private Map<String, Directive> extraDirectives;
+
+ public CLFormatter() {
+ extraDirectives = new HashMap<>();
+ }
+
+ private void checkItem(Object itm, char directive) {
+ if(itm == null)
+ throw new IllegalArgumentException(String.format("No argument provided for %c directive", directive));
+ }
+
+ public String formatString(String format, Object... params) {
+ StringBuffer sb = new StringBuffer();
+ /* Put the parameters where we can easily handle them. */
+ Tape<Object> tParams = new SingleTape(params);
+
+ doFormatString(format, sb, tParams);
+
+ return sb.toString();
+ }
+
+ private void doFormatString(String format, StringBuffer sb, Tape<Object> tParams) {
+ Matcher dirMatcher = pFormatDirective.matcher(format);
+
+ while(dirMatcher.find()) {
+ dirMatcher.appendReplacement(sb, "");
+
+ String dirName = dirMatcher.group("name");
+ String dirFunc = dirMatcher.group("funcname");
+ String dirMods = dirMatcher.group("modifiers");
+ String dirParams = dirMatcher.group("params");
+
+ CLParameters arrParams = CLParameters.fromDirective(dirParams.split("(?<!'),"), tParams);
+ CLModifiers mods = CLModifiers.fromString(dirMods);
+
+ Object item = tParams.item();
+ if(dirName == null && dirFunc != null) {
+ /*
+ * @TODO implement user-called functions.
+ */
+ continue;
+ }
+
+ switch(dirName) {
+ case "A":
+ checkItem(item, 'A');
+ handleAestheticDirective(sb, item, mods, arrParams);
+ tParams.right();
+ break;
+ case "B":
+ checkItem(item, 'B');
+ if(!(item instanceof Number)) {
+ throw new IllegalFormatConversionException('B', item.getClass());
+ }
+ handleNumberDirective(sb, mods, arrParams, -1, ((Number)item).longValue(), 2);
+ tParams.right();
+ break;
+ case "C":
+ checkItem(item, 'C');
+ handleCDirective(sb, item, mods);
+ tParams.right();
+ break;
+ case "D":
+ checkItem(item, 'D');
+ if(!(item instanceof Number)) {
+ throw new IllegalFormatConversionException('D', item.getClass());
+ }
+ handleNumberDirective(sb, mods, arrParams, -1, ((Number)item).longValue(), 10);
+ tParams.right();
+ break;
+ case "O":
+ checkItem(item, 'O');
+ if(!(item instanceof Number)) {
+ throw new IllegalFormatConversionException('O', item.getClass());
+ }
+ handleNumberDirective(sb, mods, arrParams, -1, ((Number)item).longValue(), 8);
+ tParams.right();
+ break;
+ case "R":
+ checkItem(item, 'R');
+ handleRadixDirective(sb, mods, arrParams, item);
+ tParams.right();
+ break;
+ case "X":
+ checkItem(item, 'X');
+ if(!(item instanceof Number)) {
+ throw new IllegalFormatConversionException('X', item.getClass());
+ }
+ handleNumberDirective(sb, mods, arrParams, -1, ((Number)item).longValue(), 16);
+ tParams.right();
+ break;
+ case "&":
+ handleFreshlineDirective(sb, arrParams);
+ break;
+ case "%":
+ handleLiteralDirective(sb, arrParams, "\n", '%');
+ break;
+ case "|":
+ handleLiteralDirective(sb, arrParams, "\f", '|');
+ break;
+ case "~":
+ handleLiteralDirective(sb, arrParams, "~", '~');
+ break;
+ case "*":
+ handleGotoDirective(mods, arrParams, tParams);
+ break;
+ case "^":
+ handleEscapeDirective(mods, arrParams, tParams);
+ break;
+ case "[":
+ handleConditionalDirective(sb, mods, arrParams, tParams, dirMatcher);
+ break;
+ case "]":
+ throw new IllegalArgumentException("Found conditional-end outside of conditional.");
+ case ";":
+ throw new IllegalArgumentException("Found conditional-seperator outside of conditional.");
+ case "T":
+ case "<":
+ case ">":
+ /* @TODO
+ * Figure out how to implement
+ * tabulation/justification in a
+ * reasonable manner.
+ */
+ throw new IllegalArgumentException("Layout-control directives aren't implemented yet.");
+ case "F":
+ case "E":
+ case "G":
+ case "$":
+ /* @TODO implement floating point directives. */
+ throw new IllegalArgumentException("Floating-point directives aren't implemented yet.");
+ case "S":
+ case "W":
+ /* @TODO
+ * figure out if we want to implement
+ * someting for these directives instead
+ * of punting.
+ * */
+ throw new IllegalArgumentException("S and W aren't implemented. Use A instead");
+ default:
+ String msg = String.format("Unknown format directive '%s'", dirName);
+ throw new UnknownFormatConversionException(msg);
+ }
+ }
+
+ dirMatcher.appendTail(sb);
+ }
+
+ private void handleCDirective(StringBuffer buff, Object parm, CLModifiers mods) {
+ if(!(parm instanceof Character)) {
+ throw new IllegalFormatConversionException('C', parm.getClass());
+ }
+
+ char ch = (Character) parm;
+ int codepoint = (int) ch;
+
+ if(mods.colonMod) {
+ /*
+ * Colon mod means print Unicode character name.
+ */
+ buff.append(Character.getName(codepoint));
+ } else {
+ buff.append(ch);
+ }
+ }
+
+ private void handleFreshlineDirective(StringBuffer buff, CLParameters params) {
+ int nTimes = 1;
+
+ if(params.length() > 1) {
+ nTimes = params.getInt(0, "occurance count", '&');
+ }
+
+ if(buff.charAt(buff.length() - 1) == '\n') nTimes -= 1;
+
+ for(int i = 0; i < nTimes; i++) {
+ buff.append("\n");
+ }
+ }
+
+ private void handleLiteralDirective(StringBuffer buff, CLParameters params, String lit, char directive) {
+ int nTimes = 1;
+
+ if(params.length() > 1) {
+ nTimes = params.getInt(0, "occurance count", directive);
+ }
+
+ for(int i = 0; i < nTimes; i++) {
+ buff.append(lit);
+ }
+ }
+
+ private void handleNumberDirective(StringBuffer buff, CLModifiers mods, CLParameters params, int argidx, long val, int radix) {
+ /*
+ * Initialize the two padding related parameters, and
+ * then fill them in from the directive parameters if
+ * they are present.
+ */
+ int mincol = 0;
+ char padchar = ' ';
+ if(params.length() > (argidx + 2)) {
+ mincol = params.getIntDefault(argidx + 1, "minimum column count", 'R', 0);
+ }
+ if(params.length() > (argidx + 3)) {
+ padchar = params.getCharDefault(argidx + 2, "padding character", 'R', ' ');
+ }
+
+ if(mods.colonMod) {
+ /*
+ * We're doing commas, so check if the two
+ * comma-related parameters were supplied.
+ */
+ int commaInterval = 0;
+ char commaChar = ',';
+ if(params.length() > (argidx + 3)) {
+ commaChar = params.getCharDefault((argidx + 3), "comma character", 'R', ' ');
+ }
+ if(params.length() > (argidx + 4)) {
+ commaInterval = params.getIntDefault((argidx + 4), "comma interval", 'R', 0);
+ }
+
+ NumberUtils.toCommaString(val, mincol, padchar, commaInterval, commaChar, mods.atMod, radix);
+ } else {
+ NumberUtils.toNormalString(val, mincol, padchar, mods.atMod, radix);
+ }
+ }
+
+ private void handleRadixDirective(StringBuffer buff, CLModifiers mods, CLParameters params, Object arg) {
+ if(!(arg instanceof Number)) {
+ throw new IllegalFormatConversionException('R', arg.getClass());
+ }
+
+ /*
+ * @TODO see if this is the way we want to do this.
+ */
+ long val = ((Number)arg).longValue();
+
+ if(params.length() == 0) {
+ if(mods.atMod) {
+ buff.append(NumberUtils.toRoman((Long)val, mods.colonMod));
+ } else if(mods.colonMod) {
+ buff.append(NumberUtils.toOrdinal(val));
+ } else {
+ buff.append(NumberUtils.toCardinal(val));
+ }
+ } else {
+ if(params.length() < 1)
+ throw new IllegalArgumentException("R directive requires at least one parameter, the radix");
+
+ int radix = params.getInt(0, "radix", 'R');
+
+ handleNumberDirective(buff, mods, params, 0, val, radix);
+ }
+ }
+
+ private void handleAestheticDirective(StringBuffer buff, Object item, CLModifiers mods, CLParameters params) {
+ int mincol = 0, colinc = 1, minpad = 0;
+ char padchar = ' ';
+
+ if(params.length() > 1) {
+ mincol = params.getIntDefault(0, "minimum column count", 'A', 0);
+ }
+
+ if(params.length() < 4) {
+ throw new IllegalArgumentException("Must provide either zero, one or four arguments to A directive");
+ }
+
+ colinc = params.getIntDefault(1, "padding increment", 'A', 1);
+ minpad = params.getIntDefault(2, "minimum amount of padding", 'A', 0);
+ padchar = params.getCharDefault(3, "padding character", 'A', ' ');
+
+ StringBuilder work = new StringBuilder();
+
+ if(mods.atMod) {
+ for(int i = 0; i < minpad; i++) {
+ work.append(padchar);
+ }
+
+ for(int i = work.length(); i < mincol; i++) {
+ for(int k = 0; k < colinc; k++) {
+ work.append(padchar);
+ }
+ }
+ }
+
+ work.append(item.toString());
+
+ if(!mods.atMod) {
+ for(int i = 0; i < minpad; i++) {
+ work.append(padchar);
+ }
+
+ for(int i = work.length(); i < mincol; i++) {
+ for(int k = 0; k < colinc; k++) {
+ work.append(padchar);
+ }
+ }
+ }
+ }
+
+ private void handleGotoDirective(CLModifiers mods, CLParameters params, Tape<Object> formatParams) {
+ if(mods.colonMod) {
+ int num = 1;
+ if(params.length() > 1) {
+ num = params.getIntDefault(0, "number of arguments backward", '*', 1);
+ }
+
+ formatParams.left(num);
+ } else if(mods.atMod) {
+ int num = 0;
+ if(params.length() > 1) {
+ num = params.getIntDefault(0, "argument index", '*', 0);
+ }
+
+ formatParams.first();
+ formatParams.right(num);
+ } else {
+ int num = 1;
+ if(params.length() > 1) {
+ num = params.getIntDefault(0, "number of arguments forward", '*', 1);
+ }
+
+ formatParams.right(num);
+ }
+ }
+
+ private void handleConditionalDirective(StringBuffer sb, CLModifiers mods, CLParameters arrParams, Tape<Object> formatParams, Matcher dirMatcher) {
+ StringBuffer condBody = new StringBuffer();
+
+ List<String> clauses = new ArrayList<>();
+ String defClause = null;
+ boolean isDefault = false;
+
+ while(dirMatcher.find()) {
+ /* Process a list of clauses. */
+ String dirName = dirMatcher.group("name");
+ String dirMods = dirMatcher.group("modifiers");
+
+ if(dirName != null) {
+ /* Append everything up to this directive. */
+ dirMatcher.appendReplacement(condBody, "");
+
+ if(dirName.equals("]")) {
+ /* End the conditional. */
+ String clause = condBody.toString();
+ if(isDefault) {
+ defClause = clause;
+ } else {
+ clauses.add(clause);
+ }
+
+ break;
+ } else if(dirName.equals(";")) {
+ /* End the clause. */
+ String clause = condBody.toString();
+ if(isDefault) {
+ defClause = clause;
+ } else {
+ clauses.add(clause);
+ }
+
+ /* Mark the next clause as the default. */
+ if(dirMods.contains(":")) {
+ isDefault = true;
+ }
+ } else {
+ /* Not a special directive. */
+ condBody.append(dirMatcher.group());
+ }
+ }
+ }
+
+ Object par = formatParams.item();
+ if(mods.colonMod) {
+ formatParams.right();
+
+ if(par == null) {
+ throw new IllegalArgumentException("No parameter provided for [ directive.");
+ } else if(!(par instanceof Boolean)) {
+ throw new IllegalFormatConversionException('[', par.getClass());
+ }
+ boolean res = (Boolean)par;
+
+ String fmt;
+ if(res) fmt = clauses.get(1);
+ else fmt = clauses.get(0);
+
+ doFormatString(fmt, sb, formatParams);
+ } else if(mods.atMod) {
+ if(par == null) {
+ throw new IllegalArgumentException("No parameter provided for [ directive.");
+ } else if(!(par instanceof Boolean)) {
+ throw new IllegalFormatConversionException('[', par.getClass());
+ }
+ boolean res = (Boolean)par;
+
+ if(res) {
+ doFormatString(clauses.get(0), sb, formatParams);
+ } else {
+ formatParams.right();
+ }
+ } else {
+ int res;
+ if(arrParams.length() > 1) {
+ res = arrParams.getInt(0, "conditional choice", '[');
+ } else {
+ if(par == null) {
+ throw new IllegalArgumentException("No parameter provided for [ directive.");
+ } else if(!(par instanceof Number)) {
+ throw new IllegalFormatConversionException('[', par.getClass());
+ }
+ res = ((Number)par).intValue();
+
+ formatParams.right();
+ }
+
+ if(res < 0 || res > clauses.size()) {
+ if(defClause != null) doFormatString(defClause, sb, formatParams);
+ } else {
+ doFormatString(clauses.get(res), sb, formatParams);
+ }
+ }
+ return;
+ }
+
+ private void handleEscapeDirective(CLModifiers mods, CLParameters params, Tape<Object> formatParams) {
+ boolean shouldExit;
+
+ switch(params.length()) {
+ case 0:
+ shouldExit = formatParams.size() == 0;
+ break;
+ case 1:
+ int num = params.getInt(0, "condition count", '^');
+ shouldExit = num == 0;
+ break;
+ case 2:
+ int left = params.getInt(0, "left-hand condition", '^');
+ int right = params.getInt(1, "right-hand condition", '^');
+ shouldExit = left == right;
+ break;
+ case 3:
+ default:
+ int low = params.getInt(0, "lower-bound condition", '^');
+ int mid = params.getInt(1, "interval condition", '^');
+ int high = params.getInt(2, "upper-bound condition", '^');
+ shouldExit = (low <= mid) && (mid <= high);
+ break;
+ }
+
+ /* At negates it. */
+ if(mods.atMod) shouldExit = !shouldExit;
+
+ if(shouldExit) throw new EscapeException(mods.colonMod);
+ }
+
+
+}
diff --git a/base/src/main/java/bjc/utils/ioutils/CLParameters.java b/base/src/main/java/bjc/utils/ioutils/CLParameters.java new file mode 100644 index 0000000..e4bb6fb --- /dev/null +++ b/base/src/main/java/bjc/utils/ioutils/CLParameters.java @@ -0,0 +1,109 @@ +package bjc.utils.ioutils; + +import java.util.ArrayList; +import java.util.List; + +import bjc.utils.esodata.Tape; + +/** + * Represents a set of parameters to a CL format directive. + * + * @author Benjamin Culkin + */ +public class CLParameters { + private String[] params; + + public CLParameters(String[] params) { + this.params = params; + } + + public int length() { + return params.length; + } + + /** + * Creates a set of parameters from an array of parameters. + * + * Mostly, this just fills in V and # parameters. + * + * @param params + * The parameters of the directive. + * @param dirParams + * The parameters of the format string. + * + * @return A set of CL parameters. + */ + public static CLParameters fromDirective(String[] params, Tape<Object> dirParams) { + List<String> parameters = new ArrayList<>(); + + for(String param : params) { + if(param.equalsIgnoreCase("V")) { + Object par = dirParams.item(); + boolean succ = dirParams.right(); + + if(par == null) { + throw new IllegalArgumentException("Expected a format parameter for V inline parameter"); + } + + if(par instanceof Number) { + int val = ((Number)par).intValue(); + + parameters.add(Integer.toString(val)); + } else if(par instanceof Character) { + char ch = ((Character)par); + + parameters.add(Character.toString(ch)); + } else { + throw new IllegalArgumentException("Incorrect type of parameter for V inline parameter"); + } + } else if(param.equals("#")) { + parameters.add(Integer.toString(dirParams.position())); + } else { + parameters.add(param); + } + } + + return new CLParameters(parameters.toArray(new String[0])); + } + + public char getCharDefault(int idx, String paramName, char directive, char def) { + if(!params[idx].equals("")) { + return getChar(idx, paramName, directive); + } + + return def; + } + + public char getChar(int idx, String paramName, char directive) { + String param = params[idx]; + + if(!param.startsWith("'")) { + throw new IllegalArgumentException(String.format("Invalid %s %s to %c directive", paramName, param, directive)); + } + + return param.charAt(1); + } + + public int getIntDefault(int idx, String paramName, char directive, int def) { + if(!params[idx].equals("")) { + + } + + return def; + } + + public int getInt(int idx, String paramName, char directive) { + String param = params[idx]; + + try { + return Integer.parseInt(param); + } catch (NumberFormatException nfex) { + String msg = String.format("Invalid %s %s to %c directive", paramName, param, directive); + + IllegalArgumentException iaex = new IllegalArgumentException(msg); + iaex.initCause(nfex); + + throw iaex; + } + } +} diff --git a/base/src/main/java/bjc/utils/ioutils/NumberUtils.java b/base/src/main/java/bjc/utils/ioutils/NumberUtils.java new file mode 100644 index 0000000..1b754e2 --- /dev/null +++ b/base/src/main/java/bjc/utils/ioutils/NumberUtils.java @@ -0,0 +1,405 @@ +package bjc.utils.ioutils; + +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.LongPredicate; + +import static java.util.Map.Entry; + +public class NumberUtils { + /* + * @TODO Use U+305 for large roman numerals, as well as excels 'concise' + * numerals (as implemented by roman()). + */ + public static String toRoman(long number, boolean classic) { + StringBuilder work = new StringBuilder(); + + long currNumber = number; + + if(currNumber == 0) { + return "N"; + } + + if(currNumber < 0) { + currNumber *= -1; + + work.append("-"); + } + + if(currNumber >= 1000) { + int numM = (int)(currNumber / 1000); + currNumber = currNumber % 1000; + + for(int i = 0; i < numM; i++) { + work.append("M"); + } + } + + if(currNumber >= 900 && !classic) { + currNumber = currNumber % 900; + + work.append("CM"); + } + + if(currNumber >= 500) { + currNumber = currNumber % 500; + + work.append("D"); + } + + if(currNumber >= 400 && !classic) { + currNumber = currNumber % 400; + + work.append("CD"); + } + + if(currNumber >= 100) { + int numC = (int)(currNumber / 100); + currNumber = currNumber % 100; + + for(int i = 0; i < numC; i++) { + work.append("C"); + } + } + + if(currNumber >= 90 && !classic) { + currNumber = currNumber % 90; + + work.append("XC"); + } + + if(currNumber >= 50) { + currNumber = currNumber % 50; + + work.append("L"); + } + + if(currNumber >= 40 && !classic) { + currNumber = currNumber % 40; + + work.append("XL"); + } + + if(currNumber >= 10) { + int numX = (int)(currNumber / 10); + currNumber = currNumber % 10; + + for(int i = 0; i < numX; i++) { + work.append("X"); + } + } + + if(currNumber >= 9 && !classic) { + currNumber = currNumber % 9; + + work.append("IX"); + } + + if(currNumber >= 5) { + currNumber = currNumber % 5; + + work.append("V"); + } + + if(currNumber >= 4 && !classic) { + currNumber = currNumber % 4; + + work.append("IV"); + } + + if(currNumber >= 1) { + int numI = (int)(currNumber / 1); + currNumber = currNumber % 1; + + for(int i = 0; i < numI; i++) { + work.append("I"); + } + } + + return work.toString(); + } + + public static String toCardinal(long number) { + return toCardinal(number, null); + } + + private static String[] cardinals = new String[] { + "zero", "one", "two", "three", "four", "five", "six", "seven", + "eight", "nine", "ten", "eleven", "twelve", "thirteen", + "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", + "nineteen", "twenty", + }; + + public static class CardinalState { + public final Map<Long, String> customNumbers; + public final Map<LongPredicate, BiFunction<Long, CardinalState, String>> customScales; + + public CardinalState(Map<Long, String> customNumbers, Map<LongPredicate, BiFunction<Long, CardinalState, String>> customScales) { + this.customNumbers = customNumbers; + this.customScales = customScales; + } + + public String handleCustom(long number) { + if(customNumbers.containsKey(number)) { + return customNumbers.get(number); + } + + for(Entry<LongPredicate, BiFunction<Long, CardinalState, String>> ent : customScales.entrySet()) { + if(ent.getKey().test(number)) { + return ent.getValue().apply(number, this); + } + } + + return null; + } + } + + public static String toCardinal(long number, CardinalState custom) { + if(custom != null) { + String res = custom.handleCustom(number); + + if(res != null) return res; + } + + if(number < 0) return "negative " + toCardinal(number * -1, custom); + + if(number <= 20) return cardinals[(int)number]; + + if(number < 100) { + if(number % 10 == 0) { + switch((int)number) { + case 30: + return "thirty"; + case 40: + return "forty"; + case 50: + return "fifty"; + case 60: + return "sixty"; + case 70: + return "seventy"; + case 80: + return "eighty"; + case 90: + return "ninety"; + default: + /* + * Shouldn't happen. + */ + assert(false); + } + } + + long numTens = (long)(number / 10); + long numOnes = number % 10; + + return toCardinal(numTens, custom) + "-" + toCardinal(numOnes, custom); + } + + if(number < 1000) { + long numHundreds = (long)(number / 100); + long rest = number % 100; + + return toCardinal(numHundreds, custom) + " hundred and " + toCardinal(rest, custom); + } + + long MILLION = (long)(Math.pow(10, 6)); + if(number < MILLION) { + long numThousands = (long)(number / 1000); + long rest = number % 1000; + + return toCardinal(numThousands, custom) + " thousand, " + toCardinal(rest, custom); + } + + long BILLION = (long)(Math.pow(10, 9)); + if(number < BILLION) { + long numMillions = (long)(number / MILLION); + long rest = number % MILLION; + + return toCardinal(numMillions, custom) + " million, " + toCardinal(rest, custom); + } + + long TRILLION = (long)(Math.pow(10, 12)); + if(number < TRILLION) { + long numBillions = (long)(number / BILLION); + long rest = number % BILLION; + + return toCardinal(numBillions, custom) + " billion, " + toCardinal(rest, custom); + } + + throw new IllegalArgumentException("Numbers greater than or equal to 1 trillion are not supported yet."); + } + + public static String toOrdinal(long number) { + if(number < 0) { + return "minus " + toOrdinal(number); + } + + if(number < 20) { + switch((int)number) { + case 0: + return "zeroth"; + case 1: + return "first"; + case 2: + return "second"; + case 3: + return "third"; + case 4: + return "fourth"; + case 5: + return "fifth"; + case 6: + return "sixth"; + case 7: + return "seventh"; + case 8: + return "eighth"; + case 9: + return "ninth"; + case 10: + return "tenth"; + case 11: + return "eleventh"; + case 12: + return "twelfth"; + case 13: + return "thirteenth"; + case 14: + return "fourteenth"; + case 15: + return "fifteenth"; + case 16: + return "sixteenth"; + case 17: + return "seventeenth"; + case 18: + return "eighteenth"; + case 19: + return "nineteenth"; + default: + /* + * Shouldn't happen. + */ + assert(false); + } + } + + if(number < 100) { + if(number % 10 == 0) { + switch((int)number) { + case 20: + return "twentieth"; + case 30: + return "thirtieth"; + case 40: + return "fortieth"; + case 50: + return "fiftieth"; + case 60: + return "sixtieth"; + case 70: + return "seventieth"; + case 80: + return "eightieth"; + case 90: + return "ninetieth"; + } + } + + long numPostfix = number % 10; + return toCardinal(number - numPostfix) + "-" + toOrdinal(numPostfix); + } + + long procNum = number % 100; + long tens = (long)(procNum / 10); + long ones = procNum % 10; + + if(tens == 1) { + return Long.toString(number) + "th"; + } + + switch((int)ones) { + case 1: + return Long.toString(number) + "st"; + case 2: + return Long.toString(number) + "nd"; + case 3: + return Long.toString(number) + "rd"; + default: + return Long.toString(number) + "th"; + } + } + + private static char[] radixChars = new char[62]; + static { + int idx = 0; + + for(char i = 0; i < 10; i++) { + radixChars[idx] = (char)('0' + i); + + idx += 1; + } + + for(char i = 0; i < 26; i++) { + radixChars[idx] = (char)('A' + i); + + idx += 1; + } + + for(char i = 0; i < 26; i++) { + radixChars[idx] = (char)('a' + i); + + idx += 1; + } + } + + public static String toCommaString(long val, int mincols, char padchar, int commaInterval, char commaChar, boolean signed, int radix) { + if(radix > radixChars.length) { + throw new IllegalArgumentException(String.format("Radix %d is larger than largest supported radix %d", radix, radixChars.length)); + } + + StringBuilder work = new StringBuilder(); + + boolean isNeg = false; + long currVal = val; + if(currVal < 0) { + isNeg = true; + currVal *= -1; + } + + if(currVal == 0) { + work.append(radixChars[0]); + } else { + int valCounter = 0; + + while(currVal != 0) { + valCounter += 1; + + int radDigit = (int)(currVal % radix); + work.append(radixChars[radDigit]); + currVal = (long)(currVal / radix); + + if(commaInterval != 0 && valCounter % commaInterval == 0) work.append(commaChar); + } + } + + if(isNeg) work.append("-"); + else if(signed) work.append("+"); + + work.reverse(); + + /* @TODO Should we have some way to specify how to pad? */ + if(work.length() < mincols) { + for(int i = work.length(); i < mincols; i++) { + work.append(padchar); + } + } + + return work.toString(); + } + + public static String toNormalString(long val, int mincols, char padchar, boolean signed, int radix) { + return toCommaString(val, mincols, padchar, 0, ',', signed, radix); + } +} diff --git a/base/src/main/java/bjc/utils/ioutils/Prompter.java b/base/src/main/java/bjc/utils/ioutils/Prompter.java new file mode 100644 index 0000000..a6ec4c0 --- /dev/null +++ b/base/src/main/java/bjc/utils/ioutils/Prompter.java @@ -0,0 +1,47 @@ +package bjc.utils.ioutils; + +import java.io.PrintStream; + +import bjc.utils.ioutils.blocks.TriggeredBlockReader; + +/** + * A runnable for use with {@link TriggeredBlockReader} to prompt the user for + * input. + * + * @author bjculkin + * + */ +public final class Prompter implements Runnable { + private String promt; + private final PrintStream printer; + + /** + * Create a new prompter using the specified prompt. + * + * @param prompt + * The prompt to present. + * + * @param output + * The stream to print the prompt on. + */ + public Prompter(final String prompt, final PrintStream output) { + promt = prompt; + + printer = output; + } + + /** + * Set the prompt this prompter uses. + * + * @param prompt + * The prompt this prompter uses. + */ + public void setPrompt(final String prompt) { + promt = prompt; + } + + @Override + public void run() { + printer.print(promt); + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/ioutils/RegexStringEditor.java b/base/src/main/java/bjc/utils/ioutils/RegexStringEditor.java new file mode 100644 index 0000000..71f6782 --- /dev/null +++ b/base/src/main/java/bjc/utils/ioutils/RegexStringEditor.java @@ -0,0 +1,230 @@ +package bjc.utils.ioutils; + +import java.util.function.BiFunction; +import java.util.function.UnaryOperator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import bjc.utils.data.Toggle; +import bjc.utils.data.ValueToggle; +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.IList; +import bjc.utils.functypes.ID; + +/** + * Editor methods for strings based off the command language for the Sam editor. + * + * @author EVE + * + */ +public class RegexStringEditor { + private static final UnaryOperator<String> SID = ID.id(); + + /** + * Replace every occurrence of the pattern with the result of applying + * the action to the string matched by the pattern. + * + * @param input + * The input string to process. + * + * @param patt + * The pattern to match the string against. + * + * @param action + * The action to transform matches with. + * + * @return The string, with matches replaced with the action. + */ + public static String onOccurances(final String input, final Pattern patt, final UnaryOperator<String> action) { + return reduceOccurances(input, patt, SID, action); + } + + /** + * Replace every occurrence between the patterns with the result of + * applying the action to the strings between the patterns. + * + * @param input + * The input string to process. + * + * @param patt + * The pattern to match the string against. + * + * @param action + * The action to transform matches with. + * + * @return The string, with strings between the matches replaced with + * the action. + */ + public static String betweenOccurances(final String input, final Pattern patt, + final UnaryOperator<String> action) { + return reduceOccurances(input, patt, action, SID); + } + + /** + * Execute actions between and on matches of a regular expression. + * + * @param input + * The input string. + * + * @param rPatt + * The pattern to match against the string. + * + * @param betweenAction + * The function to execute between matches of the string. + * + * @param onAction + * The function to execute on matches of the string. + * + * @return The string, with both actions applied. + */ + public static String reduceOccurances(final String input, final Pattern rPatt, + final UnaryOperator<String> betweenAction, final UnaryOperator<String> onAction) { + /* + * Get all of the occurances. + */ + final IList<String> occurances = listOccurances(input, rPatt); + + /* + * Execute the correct action on every occurance. + */ + final Toggle<UnaryOperator<String>> actions = new ValueToggle<>(onAction, betweenAction); + final BiFunction<String, StringBuilder, StringBuilder> reducer = (strang, state) -> { + return state.append(actions.get().apply(strang)); + }; + + /* + * Convert the list back to a string. + */ + return occurances.reduceAux(new StringBuilder(), reducer, StringBuilder::toString); + } + + /** + * Execute actions between and on matches of a regular expression. + * + * @param input + * The input string. + * + * @param rPatt + * The pattern to match against the string. + * + * @param betweenAction + * The function to execute between matches of the string. + * + * @param onAction + * The function to execute on matches of the string. + * + * @return The string, with both actions applied. + */ + public static IList<String> mapOccurances(final String input, final Pattern rPatt, + final UnaryOperator<String> betweenAction, final UnaryOperator<String> onAction) { + /* + * Get all of the occurances. + */ + final IList<String> occurances = listOccurances(input, rPatt); + + /* + * Execute the correct action on every occurance. + */ + final Toggle<UnaryOperator<String>> actions = new ValueToggle<>(onAction, betweenAction); + return occurances.map(strang -> actions.get().apply(strang)); + } + + /** + * Separate a string into match/non-match segments. + * + * @param input + * The string to separate. + * + * @param rPatt + * The pattern to use for separation. + * + * @return The string, as a list of match/non-match segments, + * starting/ending with a non-match segment. + */ + public static IList<String> listOccurances(final String input, final Pattern rPatt) { + final IList<String> res = new FunctionalList<>(); + + /* + * Create the matcher and work buffer. + */ + final Matcher matcher = rPatt.matcher(input); + StringBuffer work = new StringBuffer(); + + /* + * For every match. + */ + while (matcher.find()) { + final String match = matcher.group(); + + /* + * Append the text until the match to the buffer. + */ + matcher.appendReplacement(work, ""); + + res.add(work.toString()); + res.add(match); + + /* + * Clear the buffer. + */ + work = new StringBuffer(); + } + + /* + * Add the text after the last match to the buffer. + */ + matcher.appendTail(work); + res.add(work.toString()); + + return res; + } + + /** + * Apply an operation to a string if it matches a regular expression. + * + * @param input + * The input string. + * + * @param patt + * The pattern to match against it. + * + * @param action + * The action to execute if it matches. + * + * @return The string, modified by the action if the pattern matched. + */ + public static String ifMatches(final String input, final Pattern patt, final UnaryOperator<String> action) { + final Matcher matcher = patt.matcher(input); + + if (matcher.matches()) { + return action.apply(input); + } else { + return input; + } + } + + /** + * Apply an operation to a string if it matches a regular expression. + * + * @param input + * The input string. + * + * @param patt + * The pattern to match against it. + * + * @param action + * The action to execute if it doesn't match. + * + * @return The string, modified by the action if the pattern didn't + * match. + */ + public static String ifNotMatches(final String input, final Pattern patt, final UnaryOperator<String> action) { + final Matcher matcher = patt.matcher(input); + + if (matcher.matches()) { + return input; + } else { + return action.apply(input); + } + } +} diff --git a/base/src/main/java/bjc/utils/ioutils/RuleBasedConfigReader.java b/base/src/main/java/bjc/utils/ioutils/RuleBasedConfigReader.java new file mode 100644 index 0000000..7c5205b --- /dev/null +++ b/base/src/main/java/bjc/utils/ioutils/RuleBasedConfigReader.java @@ -0,0 +1,265 @@ +package bjc.utils.ioutils; + +import java.io.InputStream; +import java.util.InputMismatchException; +import java.util.Scanner; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import bjc.utils.data.IHolder; +import bjc.utils.data.IPair; +import bjc.utils.data.Identity; +import bjc.utils.data.Pair; +import bjc.utils.exceptions.UnknownPragmaException; +import bjc.utils.funcdata.FunctionalMap; +import bjc.utils.funcdata.FunctionalStringTokenizer; +import bjc.utils.funcdata.IMap; + +/** + * This class parses a rules based config file, and uses it to drive a provided + * set of actions + * + * @author ben + * + * @param <E> + * The type of the state object to use + * + */ +public class RuleBasedConfigReader<E> { + /* Function to execute when starting a rule. + * Takes the tokenizer, and a pair of the read token and application state + */ + private BiConsumer<FunctionalStringTokenizer, IPair<String, E>> start; + + /* + * Function to use when continuing a rule + * Takes a tokenizer and application state + */ + private BiConsumer<FunctionalStringTokenizer, E> continueRule; + + /* + * Function to use when ending a rule + * Takes an application state + */ + private Consumer<E> end; + + /* + * Map of pragma names to pragma actions + * Pragma actions are functions taking a tokenizer and application state + */ + private final IMap<String, BiConsumer<FunctionalStringTokenizer, E>> pragmas; + + /** + * Create a new rule-based config reader + * + * @param start + * The action to fire when starting a rule + * @param continueRule + * The action to fire when continuing a rule + * @param end + * The action to fire when ending a rule + */ + public RuleBasedConfigReader(final BiConsumer<FunctionalStringTokenizer, IPair<String, E>> start, + final BiConsumer<FunctionalStringTokenizer, E> continueRule, final Consumer<E> end) { + this.start = start; + this.continueRule = continueRule; + this.end = end; + + this.pragmas = new FunctionalMap<>(); + } + + /** + * Add a pragma to this reader + * + * @param name + * The name of the pragma to add + * @param action + * The function to execute when this pragma is read + */ + public void addPragma(final String name, final BiConsumer<FunctionalStringTokenizer, E> action) { + if (name == null) throw new NullPointerException("Pragma name must not be null"); + else if (action == null) throw new NullPointerException("Pragma action must not be null"); + + pragmas.put(name, action); + } + + private void continueRule(final E state, final boolean isRuleOpen, final String line) { + // Make sure our input is correct + if (isRuleOpen == false) + throw new InputMismatchException("Cannot continue rule with no rule open"); + else if (continueRule == null) + throw new InputMismatchException("Rule continuation not supported for current grammar"); + + /* + * Accept the rule + */ + continueRule.accept(new FunctionalStringTokenizer(line.substring(1), " "), state); + } + + private boolean endRule(final E state, final boolean isRuleOpen) { + /* + * Ignore blank line without an open rule + */ + if (isRuleOpen == false) + /* + * Do nothing + */ + return false; + else { + /* + * Nothing happens on rule end + */ + if (end != null) { + /* + * Process the rule ending + */ + end.accept(state); + } + + /* + * Return a closed rule + */ + return false; + } + } + + /** + * Run a stream through this reader + * + * @param input + * The stream to get input + * @param initialState + * The initial state of the reader + * @return The final state of the reader + */ + public E fromStream(final InputStream input, final E initialState) { + if (input == null) throw new NullPointerException("Input stream must not be null"); + + /* + * Application state: We're giving this back later + */ + final E state = initialState; + + /* + * Prepare our input source + */ + try (Scanner source = new Scanner(input)) { + source.useDelimiter("\n"); + /* + * This is true when a rule's open + */ + final IHolder<Boolean> isRuleOpen = new Identity<>(false); + + /* + * Do something for every line of the file + */ + source.forEachRemaining((line) -> { + /* + * Skip comment lines + */ + if (line.startsWith("#") || line.startsWith("//")) + /* + * It's a comment + */ + return; + else if (line.equals("")) { + /* + * End the rule + */ + isRuleOpen.replace(endRule(state, isRuleOpen.getValue())); + } else if (line.startsWith("\t")) { + /* + * Continue the rule + */ + continueRule(state, isRuleOpen.getValue(), line); + } else { + /* + * Open a rule + */ + isRuleOpen.replace(startRule(state, isRuleOpen.getValue(), line)); + } + }); + } + + /* + * Return the state that the user has created + */ + return state; + } + + /** + * Set the action to execute when continuing a rule + * + * @param continueRule + * The action to execute on continuation of a rule + */ + public void setContinueRule(final BiConsumer<FunctionalStringTokenizer, E> continueRule) { + this.continueRule = continueRule; + } + + /** + * Set the action to execute when ending a rule + * + * @param end + * The action to execute on ending of a rule + */ + public void setEndRule(final Consumer<E> end) { + this.end = end; + } + + /** + * Set the action to execute when starting a rule + * + * @param start + * The action to execute on starting of a rule + */ + public void setStartRule(final BiConsumer<FunctionalStringTokenizer, IPair<String, E>> start) { + if (start == null) throw new NullPointerException("Action on rule start must be non-null"); + + this.start = start; + } + + private boolean startRule(final E state, boolean isRuleOpen, final String line) { + /* + * Create the line tokenizer + */ + final FunctionalStringTokenizer tokenizer = new FunctionalStringTokenizer(line, " "); + + /* + * Get the initial token + */ + final String nextToken = tokenizer.nextToken(); + + /* + * Handle pragmas + */ + if (nextToken.equals("pragma")) { + /* + * Get the pragma name + */ + final String token = tokenizer.nextToken(); + + /* + * Handle pragmas + */ + pragmas.getOrDefault(token, (tokenzer, stat) -> { + throw new UnknownPragmaException("Unknown pragma " + token); + }).accept(tokenizer, state); + } else { + /* + * Make sure input is correct + */ + if (isRuleOpen == true) + throw new InputMismatchException("Nested rules are currently not supported"); + + /* + * Start a rule + */ + start.accept(tokenizer, new Pair<>(nextToken, state)); + + isRuleOpen = true; + } + + return isRuleOpen; + } +} diff --git a/base/src/main/java/bjc/utils/ioutils/RuleBasedReaderPragmas.java b/base/src/main/java/bjc/utils/ioutils/RuleBasedReaderPragmas.java new file mode 100644 index 0000000..e26a7ee --- /dev/null +++ b/base/src/main/java/bjc/utils/ioutils/RuleBasedReaderPragmas.java @@ -0,0 +1,100 @@ +package bjc.utils.ioutils; + +import java.util.function.BiConsumer; + +import bjc.utils.exceptions.PragmaFormatException; +import bjc.utils.funcdata.FunctionalStringTokenizer; +import bjc.utils.funcutils.ListUtils; + +/** + * Contains factory methods for common pragma types + * + * @author ben + * + */ +public class RuleBasedReaderPragmas { + + /** + * Creates a pragma that takes a single integer argument + * + * @param <StateType> + * The type of state that goes along with this pragma + * @param name + * The name of this pragma, for error message purpose + * @param consumer + * The function to invoke with the parsed integer + * @return A pragma that functions as described above. + */ + public static <StateType> BiConsumer<FunctionalStringTokenizer, StateType> buildInteger(final String name, + final BiConsumer<Integer, StateType> consumer) { + return (tokenizer, state) -> { + /* + * Check our input is correct + */ + if (!tokenizer.hasMoreTokens()) { + String fmt = "Pragma %s requires one integer argument"; + + throw new PragmaFormatException(String.format(fmt, name)); + } + + /* + * Read the argument + */ + final String token = tokenizer.nextToken(); + + try { + /* + * Run the pragma + */ + consumer.accept(Integer.parseInt(token), state); + } catch (final NumberFormatException nfex) { + /* + * Tell the user their argument isn't correct + */ + String fmt = "Argument %s to %s pragma isn't a valid integer, and this pragma requires an integer argument."; + + final PragmaFormatException pfex = new PragmaFormatException(String.format(fmt, token, name)); + + pfex.initCause(nfex); + + throw pfex; + } + }; + } + + /** + * Creates a pragma that takes any number of arguments and collapses + * them all into a single string + * + * @param <StateType> + * The type of state that goes along with this pragma + * @param name + * The name of this pragma, for error message purpose + * @param consumer + * The function to invoke with the parsed string + * @return A pragma that functions as described above. + */ + public static <StateType> BiConsumer<FunctionalStringTokenizer, StateType> buildStringCollapser( + final String name, final BiConsumer<String, StateType> consumer) { + return (tokenizer, state) -> { + /* + * Check our input + */ + if (!tokenizer.hasMoreTokens()) { + String fmt = "Pragma %s requires one or more string arguments."; + + throw new PragmaFormatException(String.format(fmt, name)); + } + + /* + * Build our argument + */ + final String collapsed = ListUtils.collapseTokens(tokenizer.toList()); + + /* + * Run the pragma + */ + consumer.accept(collapsed, state); + }; + } +} diff --git a/base/src/main/java/bjc/utils/ioutils/SimpleProperties.java b/base/src/main/java/bjc/utils/ioutils/SimpleProperties.java new file mode 100644 index 0000000..e6279c4 --- /dev/null +++ b/base/src/main/java/bjc/utils/ioutils/SimpleProperties.java @@ -0,0 +1,170 @@ +package bjc.utils.ioutils; + +import java.io.InputStream; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Scanner; +import java.util.Set; + +/** + * Simple file based properties. + * + * @author EVE + * + */ +public class SimpleProperties implements Map<String, String> { + private final Map<String, String> props; + + /** + * Create a new set of simple properties. + */ + public SimpleProperties() { + props = new HashMap<>(); + } + + /** + * Load properties from the provided input stream. + * + * The format is the name, a space, then the body. + * + * All leading/trailing spaces from the name & body are removed. + * + * @param is + * The stream to read from. + * + * @param allowDuplicates + * Whether or not duplicate keys should be allowed. + */ + public void loadFrom(final InputStream is, final boolean allowDuplicates) { + try (Scanner scn = new Scanner(is)) { + while (scn.hasNextLine()) { + final String ln = scn.nextLine().trim(); + + /* + * Skip blank lines/comments + */ + if (ln.equals("")) { + continue; + } + if (ln.startsWith("#")) { + continue; + } + + final int sepIdx = ln.indexOf(' '); + + /* + * Complain about improperly formatted lines. + */ + if (sepIdx == -1) { + final String fmt = "Properties must be a name, a space, then the body.\n\tOffending line is '%s'"; + final String msg = String.format(fmt, ln); + + throw new NoSuchElementException(msg); + } + + final String name = ln.substring(0, sepIdx).trim(); + final String body = ln.substring(sepIdx).trim(); + + /* + * Complain about duplicates, if that is wanted. + */ + if (!allowDuplicates && containsKey(name)) { + final String msg = String.format("Duplicate key '%s'", name); + + throw new IllegalStateException(msg); + } + + put(name, body); + } + } + } + + /** + * Output the set of read properties. + */ + public void outputProperties() { + System.out.println("Read properties:"); + + for (final Entry<String, String> entry : entrySet()) { + System.out.printf("\t'%s'\t'%s'\n", entry.getKey(), entry.getValue()); + } + + System.out.println(); + } + + @Override + public int size() { + return props.size(); + } + + @Override + public boolean isEmpty() { + return props.isEmpty(); + } + + @SuppressWarnings("unlikely-arg-type") + @Override + public boolean containsKey(final Object key) { + return props.containsKey(key); + } + + @SuppressWarnings("unlikely-arg-type") + @Override + public boolean containsValue(final Object value) { + return props.containsValue(value); + } + + @SuppressWarnings("unlikely-arg-type") + @Override + public String get(final Object key) { + return props.get(key); + } + + @Override + public String put(final String key, final String value) { + return props.put(key, value); + } + + @SuppressWarnings("unlikely-arg-type") + @Override + public String remove(final Object key) { + return props.remove(key); + } + + @Override + public void putAll(final Map<? extends String, ? extends String> m) { + props.putAll(m); + } + + @Override + public void clear() { + props.clear(); + } + + @Override + public Set<String> keySet() { + return props.keySet(); + } + + @Override + public Collection<String> values() { + return props.values(); + } + + @Override + public Set<java.util.Map.Entry<String, String>> entrySet() { + return props.entrySet(); + } + + @Override + public boolean equals(final Object o) { + return props.equals(o); + } + + @Override + public int hashCode() { + return props.hashCode(); + } +} diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/Block.java b/base/src/main/java/bjc/utils/ioutils/blocks/Block.java new file mode 100644 index 0000000..15f3510 --- /dev/null +++ b/base/src/main/java/bjc/utils/ioutils/blocks/Block.java @@ -0,0 +1,88 @@ +package bjc.utils.ioutils.blocks; + +/** + * Represents a block of text read in from a source. + * + * @author EVE + * + */ +public class Block { + /** + * The contents of this block. + */ + public final String contents; + + /** + * The line of the source this block started on. + */ + public final int startLine; + + /** + * The line of the source this block ended on. + */ + public final int endLine; + + /** + * The number of this block. + */ + public final int blockNo; + + /** + * Create a new block. + * + * @param blockNo + * The number of this block. + * @param contents + * The contents of this block. + * @param startLine + * The line this block started on. + * @param endLine + * The line this block ended. + */ + public Block(final int blockNo, final String contents, final int startLine, final int endLine) { + this.contents = contents; + this.startLine = startLine; + this.endLine = endLine; + this.blockNo = blockNo; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + + result = prime * result + blockNo; + result = prime * result + (contents == null ? 0 : contents.hashCode()); + result = prime * result + endLine; + result = prime * result + startLine; + + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof Block)) return false; + + final Block other = (Block) obj; + + if (blockNo != other.blockNo) return false; + + if (contents == null) { + if (other.contents != null) return false; + } else if (!contents.equals(other.contents)) return false; + + if (endLine != other.endLine) return false; + if (startLine != other.startLine) return false; + + return true; + } + + @Override + public String toString() { + String fmt = "Block #%d (from lines %d to %d), length: %d characters"; + + return String.format(fmt, blockNo, startLine, endLine, contents.length()); + } +} diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/BlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/BlockReader.java new file mode 100644 index 0000000..3c695c6 --- /dev/null +++ b/base/src/main/java/bjc/utils/ioutils/blocks/BlockReader.java @@ -0,0 +1,73 @@ +package bjc.utils.ioutils.blocks; + +import java.io.IOException; +import java.util.Iterator; +import java.util.function.Consumer; + +/** + * A source of blocks of characters, marked with line numbers as to block + * start/block end. + * + * @author bjculkin + * + */ +public interface BlockReader extends AutoCloseable, Iterator<Block> { + /** + * Check if this reader has an available block. + * + * @return Whether or not another block is available. + */ + boolean hasNextBlock(); + + /** + * Get the current block. + * + * @return The current block, or null if there is no current block. + */ + Block getBlock(); + + /** + * Move to the next block. + * + * @return Whether or not the next block was successfully read. + */ + boolean nextBlock(); + + /** + * Retrieve the number of blocks that have been read so far. + * + * @return The number of blocks read so far. + */ + int getBlockCount(); + + @Override + void close() throws IOException; + + /* + * Methods with default impls. + */ + + /** + * Execute an action for each remaining block. + * + * @param action + * The action to execute for each block + */ + default void forEachBlock(final Consumer<Block> action) { + while (hasNext()) { + action.accept(next()); + } + } + + @Override + default boolean hasNext() { + return hasNextBlock(); + } + + @Override + default Block next() { + nextBlock(); + + return getBlock(); + } +} diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/BlockReaders.java b/base/src/main/java/bjc/utils/ioutils/blocks/BlockReaders.java new file mode 100644 index 0000000..8bbb89c --- /dev/null +++ b/base/src/main/java/bjc/utils/ioutils/blocks/BlockReaders.java @@ -0,0 +1,81 @@ +package bjc.utils.ioutils.blocks; + +import java.io.Reader; + +/** + * Utility methods for constructing instances of {@link BlockReader} + * + * @author bjculkin + * + */ +public class BlockReaders { + /** + * Create a new simple block reader that works off a regex. + * + * @param blockDelim + * The regex that separates blocks. + * + * @param source + * The reader to get blocks from. + * + * @return A configured simple reader. + */ + public static SimpleBlockReader simple(final String blockDelim, final Reader source) { + return new SimpleBlockReader(blockDelim, source); + } + + /** + * Create a new pushback block reader. + * + * @param src + * The block reader to read blocks from. + * + * @return A configured pushback reader. + */ + public static PushbackBlockReader pushback(final BlockReader src) { + return new PushbackBlockReader(src); + } + + /** + * Create a new triggered block reader. + * + * @param source + * The block reader to read blocks from. + * + * @param action + * The action to execute before reading a block. + * + * @return A configured triggered block reader. + */ + public static BlockReader trigger(final BlockReader source, final Runnable action) { + return new TriggeredBlockReader(source, action); + } + + /** + * Create a new layered block reader. + * + * @param primary + * The first source to read blocks from. + * + * @param secondary + * The second source to read blocks from. + * + * @return A configured layered block reader. + */ + public static BlockReader layered(final BlockReader primary, final BlockReader secondary) { + return new LayeredBlockReader(primary, secondary); + } + + /** + * Create a new serial block reader. + * + * @param readers + * The readers to pull from, in the order to pull from + * them. + * + * @return A configured serial block reader. + */ + public static BlockReader serial(final BlockReader... readers) { + return new SerialBlockReader(readers); + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/BoundBlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/BoundBlockReader.java new file mode 100644 index 0000000..b1e82d7 --- /dev/null +++ b/base/src/main/java/bjc/utils/ioutils/blocks/BoundBlockReader.java @@ -0,0 +1,61 @@ +package bjc.utils.ioutils.blocks; + +import java.io.IOException; + +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; + +public class BoundBlockReader implements BlockReader { + @FunctionalInterface + public interface Closer { + public void close() throws IOException; + } + + private BooleanSupplier checker; + private Supplier<Block> getter; + private Closer closer; + + private Block current; + + private int blockNo; + + public BoundBlockReader(BooleanSupplier blockChecker, Supplier<Block> blockGetter, Closer blockCloser) { + checker = blockChecker; + getter = blockGetter; + closer = blockCloser; + + blockNo = 0; + } + + @Override + public boolean hasNextBlock() { + return checker.getAsBoolean(); + } + + @Override + public Block getBlock() { + return current; + } + + @Override + public boolean nextBlock() { + if(checker.getAsBoolean()) { + current = getter.get(); + blockNo += 1; + + return true; + } + + return false; + } + + @Override + public int getBlockCount() { + return blockNo; + } + + @Override + public void close() throws IOException { + closer.close(); + } +} diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/FilteredBlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/FilteredBlockReader.java new file mode 100644 index 0000000..0b43f7a --- /dev/null +++ b/base/src/main/java/bjc/utils/ioutils/blocks/FilteredBlockReader.java @@ -0,0 +1,97 @@ +package bjc.utils.ioutils.blocks; + +import java.io.IOException; + +import java.util.function.Consumer; +import java.util.function.Predicate; + +public class FilteredBlockReader implements BlockReader { + /* + * The source of blocks. + */ + private BlockReader source; + + /* + * The current and next block. + * + * Both have already been checked for the predicate. + */ + private Block current; + private Block pending; + + /* + * Number of blocks that passed the predicate. + */ + private int blockNo; + + /* + * The predicate blocks must pass. + */ + private Predicate<Block> pred; + + /* + * The action to call on failure, if there is one. + */ + private Consumer<Block> failAction; + + public FilteredBlockReader(BlockReader src, Predicate<Block> predic) { + this(src, predic, null); + } + + public FilteredBlockReader(BlockReader src, Predicate<Block> predic, Consumer<Block> failAct) { + source = src; + pred = predic; + failAction = failAct; + + blockNo = 0; + } + + @Override + public boolean hasNextBlock() { + if(pending != null) return true; + + while(source.hasNextBlock()) { + /* + * Only say we have a next block if the next block would + * pass the predicate. + */ + pending = source.next(); + + if(pred.test(pending)) { + blockNo += 1; + return true; + } else { + failAction.accept(pending); + } + } + + return false; + } + + @Override + public Block getBlock() { + return current; + } + + @Override + public boolean nextBlock() { + if(pending != null || hasNextBlock()) { + current = pending; + pending = null; + + return true; + } + + return false; + } + + @Override + public int getBlockCount() { + return blockNo; + } + + @Override + public void close() throws IOException { + source.close(); + } +} diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/FlatMappedBlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/FlatMappedBlockReader.java new file mode 100644 index 0000000..f4d8439 --- /dev/null +++ b/base/src/main/java/bjc/utils/ioutils/blocks/FlatMappedBlockReader.java @@ -0,0 +1,86 @@ +package bjc.utils.ioutils.blocks; + +import java.io.IOException; + +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +/** + * A block reader that supports applying a flatmap operation to blocks. + * + * The use-case in mind for this was tokenizing blocks. + * + * @author Benjamin Culkin + */ +public class FlatMappedBlockReader implements BlockReader { + /* + * The source reader. + */ + private BlockReader reader; + + /* + * The current block, and any blocks pending from the last source block. + */ + private Iterator<Block> pending; + private Block current; + + /* + * The operator to open blocks with. + */ + private Function<Block, List<Block>> transform; + + /* + * The current block number. + */ + private int blockNo; + + public FlatMappedBlockReader(BlockReader source, Function<Block, List<Block>> trans) { + reader = source; + transform = trans; + + blockNo = 0; + } + + @Override + public boolean hasNextBlock() { + return pending.hasNext() || reader.hasNextBlock(); + } + + @Override + public Block getBlock() { + return current; + } + + @Override + public boolean nextBlock() { + /* + * Attempt to get a new pending list if the one we have isn't + * valid. + */ + while(pending == null || !pending.hasNext()) { + if(!reader.hasNext()) return false; + + pending = transform.apply(reader.next()).iterator(); + } + + /* + * Advance the iterator. + */ + current = pending.next(); + blockNo += 1; + + return true; + } + + @Override + public int getBlockCount() { + return blockNo; + } + + @Override + public void close() throws IOException { + reader.close(); + } +} diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/LayeredBlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/LayeredBlockReader.java new file mode 100644 index 0000000..967a1f2 --- /dev/null +++ b/base/src/main/java/bjc/utils/ioutils/blocks/LayeredBlockReader.java @@ -0,0 +1,81 @@ +package bjc.utils.ioutils.blocks; + +import java.io.IOException; + +/** + * A block reader that supports draining all the blocks from one reading before + * swapping to another. + * + * This is more a 'prioritize blocks from one over the other', than a 'read all + * the blocks from one, then all the blocks from the other'. If you need that, + * look at {@link SerialBlockReader}. + * + * @author bjculkin + * + */ +public class LayeredBlockReader implements BlockReader { + /* + * The readers to drain from. + */ + private final BlockReader first; + private final BlockReader second; + + /* + * The current block number. + */ + private int blockNo; + + /** + * Create a new layered block reader. + * + * @param primary + * The first source to read blocks from. + * + * @param secondary + * The second source to read blocks from. + */ + public LayeredBlockReader(final BlockReader primary, final BlockReader secondary) { + first = primary; + second = secondary; + } + + @Override + public boolean hasNextBlock() { + return first.hasNextBlock() || second.hasNextBlock(); + } + + @Override + public Block getBlock() { + final Block firstBlock = first.getBlock(); + + /* + * Only drain a block from the second reader if none are + * available in the first reader. + */ + return firstBlock == null ? second.getBlock() : firstBlock; + } + + @Override + public boolean nextBlock() { + final boolean gotFirst = first.nextBlock(); + final boolean succ = gotFirst ? gotFirst : second.nextBlock(); + + if (succ) { + blockNo += 1; + } + + return succ; + } + + @Override + public int getBlockCount() { + return blockNo; + } + + @Override + public void close() throws IOException { + second.close(); + + first.close(); + } +} diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/MappedBlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/MappedBlockReader.java new file mode 100644 index 0000000..12fa848 --- /dev/null +++ b/base/src/main/java/bjc/utils/ioutils/blocks/MappedBlockReader.java @@ -0,0 +1,54 @@ +package bjc.utils.ioutils.blocks; + +import java.io.IOException; + +import java.util.function.UnaryOperator; + +public class MappedBlockReader implements BlockReader { + private BlockReader reader; + + private Block current; + + private UnaryOperator<Block> transform; + + private int blockNo; + + public MappedBlockReader(BlockReader source, UnaryOperator<Block> trans) { + reader = source; + transform = trans; + + blockNo = 0; + } + + @Override + public boolean hasNextBlock() { + return reader.hasNextBlock(); + } + + @Override + public Block getBlock() { + return current; + } + + @Override + public boolean nextBlock() { + if(hasNextBlock()) { + current = transform.apply(reader.next()); + blockNo += 1; + + return true; + } + + return false; + } + + @Override + public int getBlockCount() { + return blockNo; + } + + @Override + public void close() throws IOException { + reader.close(); + } +} diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/PushbackBlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/PushbackBlockReader.java new file mode 100644 index 0000000..0cc9dea --- /dev/null +++ b/base/src/main/java/bjc/utils/ioutils/blocks/PushbackBlockReader.java @@ -0,0 +1,106 @@ +package bjc.utils.ioutils.blocks; + +import java.io.IOException; +import java.util.Deque; +import java.util.LinkedList; + +/** + * A block reader that supports pushing blocks onto the input queue so that they + * are provided before blocks read from an input source. + * + * @author bjculkin + * + */ +public class PushbackBlockReader implements BlockReader { + private final BlockReader source; + + /* + * The queue of pushed-back blocks. + */ + private final Deque<Block> waiting; + + private Block curBlock; + + private int blockNo; + + /** + * Create a new pushback block reader. + * + * @param src + * The block reader to use when no blocks are queued. + */ + public PushbackBlockReader(final BlockReader src) { + source = src; + + waiting = new LinkedList<>(); + } + + @Override + public boolean hasNextBlock() { + return !waiting.isEmpty() || source.hasNextBlock(); + } + + @Override + public Block getBlock() { + return curBlock; + } + + @Override + public boolean nextBlock() { + /* + * Drain pushed-back blocks first. + */ + if (!waiting.isEmpty()) { + curBlock = waiting.pop(); + + blockNo += 1; + + return true; + } else { + final boolean succ = source.nextBlock(); + curBlock = source.getBlock(); + + if (succ) { + blockNo += 1; + } + + return succ; + } + } + + @Override + public int getBlockCount() { + return blockNo; + } + + @Override + public void close() throws IOException { + source.close(); + } + + /** + * Insert a block at the back of the queue of pending blocks. + * + * @param blk + * The block to put at the back. + */ + public void addBlock(final Block blk) { + waiting.add(blk); + } + + /** + * Insert a block at the front of the queue of pending blocks. + * + * @param blk + * The block to put at the front. + */ + public void pushBlock(final Block blk) { + waiting.push(blk); + } + + @Override + public String toString() { + return String.format("PushbackBlockReader [waiting=%s, curBlock=%s, blockNo=%s]", waiting, curBlock, + blockNo); + } +} diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/SerialBlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/SerialBlockReader.java new file mode 100644 index 0000000..c229da1 --- /dev/null +++ b/base/src/main/java/bjc/utils/ioutils/blocks/SerialBlockReader.java @@ -0,0 +1,102 @@ +package bjc.utils.ioutils.blocks; + +import java.io.IOException; +import java.util.Deque; + +/** + * Provides a means of concatenating two block readers. + * + * @author bjculkin + * + */ +public class SerialBlockReader implements BlockReader { + private Deque<BlockReader> readerQueue; + + private int blockNo; + + /** + * Create a new serial block reader. + * + * @param readers + * The readers to pull from, in the order to pull from + * them. + */ + public SerialBlockReader(final BlockReader... readers) { + for (final BlockReader reader : readers) { + readerQueue.add(reader); + } + } + + @Override + public boolean hasNextBlock() { + if (readerQueue.isEmpty()) return false; + + /* + * Attempt to get a block from the first reader. + */ + boolean hasBlock = readerQueue.peek().hasNextBlock(); + boolean cont = hasBlock || readerQueue.isEmpty(); + + /* + * Close/dispose of readers until we get an open one. + */ + while (!cont) { + try { + readerQueue.pop().close(); + } catch (final IOException ioex) { + throw new IllegalStateException("Exception thrown by discarded reader", ioex); + } + + hasBlock = readerQueue.peek().hasNextBlock(); + cont = hasBlock || readerQueue.isEmpty(); + } + + return hasBlock; + } + + @Override + public Block getBlock() { + if (readerQueue.isEmpty()) + return null; + else return readerQueue.peek().getBlock(); + } + + @Override + public boolean nextBlock() { + if (readerQueue.isEmpty()) return false; + + boolean gotBlock = readerQueue.peek().nextBlock(); + boolean cont = gotBlock || readerQueue.isEmpty(); + + while (!cont) { + try { + readerQueue.pop().close(); + } catch (final IOException ioex) { + throw new IllegalStateException("Exception thrown by discarded reader", ioex); + } + + gotBlock = readerQueue.peek().nextBlock(); + cont = gotBlock || readerQueue.isEmpty(); + } + + if (cont) { + blockNo += 1; + } + + return cont; + } + + @Override + public int getBlockCount() { + return blockNo; + } + + @Override + public void close() throws IOException { + while (!readerQueue.isEmpty()) { + final BlockReader reader = readerQueue.pop(); + + reader.close(); + } + } +} diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/SimpleBlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/SimpleBlockReader.java new file mode 100644 index 0000000..734bde8 --- /dev/null +++ b/base/src/main/java/bjc/utils/ioutils/blocks/SimpleBlockReader.java @@ -0,0 +1,115 @@ +package bjc.utils.ioutils.blocks; + +import java.io.IOException; +import java.io.LineNumberReader; +import java.io.Reader; +import java.util.NoSuchElementException; +import java.util.Scanner; +import java.util.regex.Pattern; + +import bjc.utils.funcutils.StringUtils; +/** + * Simple implementation of {@link BlockReader} + * + * NOTE: The EOF marker is always treated as a delimiter. You are expected to + * handle blocks that may be shorter than you expect. + * + * @author EVE + * + */ +public class SimpleBlockReader implements BlockReader { + /* + * I/O source for blocks. + */ + private final Scanner blockReader; + + /* + * The current block. + */ + private Block currBlock; + + /* + * Info about the current block. + */ + private int blockNo; + private int lineNo; + + /** + * Create a new block reader. + * + * @param blockDelim + * The pattern that separates blocks. Note that the end + * of file is always considered to end a block. + * + * @param source + * The source to read blocks from. + */ + public SimpleBlockReader(final String blockDelim, final Reader source) { + blockReader = new Scanner(source); + + final String pattern = String.format("(?:%s)|\\Z", blockDelim); + final Pattern pt = Pattern.compile(pattern, Pattern.MULTILINE); + + blockReader.useDelimiter(pt); + + lineNo = 1; + } + + @Override + public boolean hasNextBlock() { + return blockReader.hasNext(); + } + + @Override + public Block getBlock() { + return currBlock; + } + + @Override + public boolean nextBlock() { + try { + /* + * Read in a new block, and keep the line numbers sane. + */ + final int blockStartLine = lineNo; + final String blockContents = blockReader.next(); + final int blockEndLine = lineNo + StringUtils.countMatches(blockContents, "\\R"); + + lineNo = blockEndLine; + blockNo += 1; + + currBlock = new Block(blockNo, blockContents, blockStartLine, blockEndLine); + + return true; + } catch (final NoSuchElementException nseex) { + currBlock = null; + + return false; + } + } + + @Override + public int getBlockCount() { + return blockNo; + } + + @Override + public void close() throws IOException { + blockReader.close(); + } + + /** + * Set the delimiter used to separate blocks. + * + * @param delim + * The delimiter used to separate blocks. + */ + public void setDelimiter(final String delim) { + blockReader.useDelimiter(delim); + } + + @Override + public String toString() { + return String.format("SimpleBlockReader [currBlock=%s, blockNo=%s]", currBlock, blockNo); + } +} diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/ToggledBlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/ToggledBlockReader.java new file mode 100644 index 0000000..8f39b8f --- /dev/null +++ b/base/src/main/java/bjc/utils/ioutils/blocks/ToggledBlockReader.java @@ -0,0 +1,63 @@ +package bjc.utils.ioutils.blocks; + +import java.io.IOException; + +import bjc.utils.data.BooleanToggle; + +public class ToggledBlockReader implements BlockReader { + private BlockReader leftSource; + private BlockReader rightSource; + + /* + * We choose the left source when this is true. + */ + private BooleanToggle leftToggle; + + private int blockNo; + + public ToggledBlockReader(BlockReader left, BlockReader right) { + leftSource = left; + rightSource = right; + + blockNo = 0; + + leftToggle = new BooleanToggle(); + } + + @Override + public boolean hasNextBlock() { + if(leftToggle.peek()) return leftSource.hasNextBlock(); + else return rightSource.hasNextBlock(); + } + + @Override + public Block getBlock() { + if(leftToggle.peek()) return leftSource.getBlock(); + else return rightSource.getBlock(); + } + + @Override + public boolean nextBlock() { + boolean succ; + + if(leftToggle.get()) { + succ = leftSource.nextBlock(); + } else { + succ = rightSource.nextBlock(); + } + + if(succ) blockNo += 1; + return succ; + } + + @Override + public int getBlockCount() { + return blockNo; + } + + @Override + public void close() throws IOException { + leftSource.close(); + rightSource.close(); + } +} diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/TriggeredBlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/TriggeredBlockReader.java new file mode 100644 index 0000000..3a1e393 --- /dev/null +++ b/base/src/main/java/bjc/utils/ioutils/blocks/TriggeredBlockReader.java @@ -0,0 +1,70 @@ +package bjc.utils.ioutils.blocks; + +import java.io.IOException; + +/** + * A block reader that fires an action before a block is actually read. + * + * @author bjculkin + * + */ +public class TriggeredBlockReader implements BlockReader { + private final BlockReader source; + + private int blockNo; + + /* + * The action to fire. + */ + private final Runnable action; + + /** + * Create a new triggered reader with the specified source/action. + * + * @param source + * The block reader to read blocks from. + * + * @param action + * The action to execute before reading a block. + */ + public TriggeredBlockReader(final BlockReader source, final Runnable action) { + this.source = source; + this.action = action; + + blockNo = 0; + } + + @Override + public boolean hasNextBlock() { + action.run(); + + return source.hasNextBlock(); + } + + @Override + public Block getBlock() { + return source.getBlock(); + } + + @Override + public boolean nextBlock() { + blockNo += 1; + + return source.nextBlock(); + } + + @Override + public int getBlockCount() { + return blockNo; + } + + @Override + public void close() throws IOException { + source.close(); + } + + @Override + public String toString() { + return String.format("TriggeredBlockReader [source=%s]", source); + } +} diff --git a/base/src/main/java/bjc/utils/parserutils/DoubleMatcher.java b/base/src/main/java/bjc/utils/parserutils/DoubleMatcher.java new file mode 100644 index 0000000..a885808 --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/DoubleMatcher.java @@ -0,0 +1,46 @@ +package bjc.utils.parserutils; + +import static bjc.utils.PropertyDB.applyFormat; +import static bjc.utils.PropertyDB.getRegex; + +import java.util.regex.Pattern; + +/* + * Checks if a string would pass Double.parseDouble. + * + * Uses a regex from the javadoc for Double.valueOf() + */ +class DoubleMatcher { + /* + * Unit pieces. + */ + private static final String rDecDigits = getRegex("fpDigits"); + private static final String rHexDigits = getRegex("fpHexDigits"); + private static final String rExponent = applyFormat("fpExponent", getRegex("fpExponent"), rDecDigits); + + /* + * Decimal floating point numbers. + */ + private static final String rSimpleDec = applyFormat("fpDecimalDecimal", rDecDigits, rExponent); + private static final String rSimpleIntDec = applyFormat("fpDecimalInteger", rDecDigits, rExponent); + + /* + * Hex floating point numbers. + */ + private static final String rHexInt = applyFormat("fpHexInteger", rHexDigits); + private static final String rHexDec = applyFormat("fpHexDecimal", rHexDigits); + private static final String rHexLead = applyFormat("fpHexLeader", rHexInt, rHexDec); + private static final String rHexString = applyFormat("fpHexString", rHexLead, rDecDigits); + + /* + * Floating point components. + */ + private static final String rFPLeader = getRegex("fpLeader"); + private static final String rFPNum = applyFormat("fpNumber", rSimpleIntDec, rSimpleDec, rHexString); + + /* + * Full double. + */ + private static final String rDouble = applyFormat("fpDouble", rFPLeader, rFPNum); + public static final Pattern doubleLiteral = Pattern.compile("\\A" + rDouble + "\\Z"); +} diff --git a/base/src/main/java/bjc/utils/parserutils/IPrecedent.java b/base/src/main/java/bjc/utils/parserutils/IPrecedent.java new file mode 100644 index 0000000..aa366cf --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/IPrecedent.java @@ -0,0 +1,28 @@ +package bjc.utils.parserutils; + +/** + * Represents something that has a set precedence + * + * @author ben + * + */ +@FunctionalInterface +public interface IPrecedent { + /** + * Create a new object with set precedence + * + * @param precedence + * The precedence of the object to handle + * @return A new object with set precedence + */ + public static IPrecedent newSimplePrecedent(final int precedence) { + return () -> precedence; + } + + /** + * Get the precedence of the attached object + * + * @return The precedence of the attached object + */ + public int getPrecedence(); +} diff --git a/base/src/main/java/bjc/utils/parserutils/ParserException.java b/base/src/main/java/bjc/utils/parserutils/ParserException.java new file mode 100644 index 0000000..ae33aba --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/ParserException.java @@ -0,0 +1,36 @@ +package bjc.utils.parserutils; + +/** + * General superclass for exceptions thrown during parsing. + * + * @author EVE + * + */ +public class ParserException extends Exception { + /** + * + */ + private static final long serialVersionUID = 631298568113373233L; + + /** + * Create a new exception with the provided message. + * + * @param msg + * The message for the exception. + */ + public ParserException(final String msg) { + super(msg); + } + + /** + * Create a new exception with the provided message and cause. + * + * @param msg + * The message for the exception. + * @param cause + * The cause of the exception. + */ + public ParserException(final String msg, final Exception cause) { + super(msg, cause); + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/parserutils/ShuntingYard.java b/base/src/main/java/bjc/utils/parserutils/ShuntingYard.java new file mode 100644 index 0000000..a1b5feb --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/ShuntingYard.java @@ -0,0 +1,274 @@ +package bjc.utils.parserutils; + +import java.util.Deque; +import java.util.LinkedList; +import java.util.function.Consumer; +import java.util.function.Function; + +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.FunctionalMap; +import bjc.utils.funcdata.IList; +import bjc.utils.funcdata.IMap; +import bjc.utils.funcutils.StringUtils; + +/** + * Utility to run the shunting yard algorithm on a bunch of tokens. + * + * @author ben + * + * @param <TokenType> + * The type of tokens being shunted. + */ +public class ShuntingYard<TokenType> { + /** + * A enum representing the fundamental operator types. + * + * @author ben + * + */ + public static enum Operator implements IPrecedent { + /** + * Represents addition. + */ + ADD(1), + /** + * Represents subtraction. + */ + SUBTRACT(2), + + /** + * Represents multiplication. + */ + MULTIPLY(3), + /** + * Represents division. + */ + DIVIDE(4); + + private final int precedence; + + private Operator(final int prec) { + precedence = prec; + } + + @Override + public int getPrecedence() { + return precedence; + } + } + + /* + * Function that shunts tokens. + */ + private final class TokenShunter implements Consumer<String> { + private final IList<TokenType> output; + private final Deque<String> stack; + private final Function<String, TokenType> transformer; + + public TokenShunter(final IList<TokenType> outpt, final Deque<String> stack, + final Function<String, TokenType> transformer) { + this.output = outpt; + this.stack = stack; + this.transformer = transformer; + } + + @Override + public void accept(final String token) { + /* + * Handle operators + */ + if (operators.containsKey(token)) { + /* + * Pop operators while there isn't a higher precedence one + */ + while (!stack.isEmpty() && isHigherPrec(token, stack.peek())) { + output.add(transformer.apply(stack.pop())); + } + + /* + * Put this operator onto the stack + */ + stack.push(token); + } else if (StringUtils.containsOnly(token, "\\(")) { + /* + * Handle groups of parenthesis for multiple nesting levels + */ + stack.push(token); + } else if (StringUtils.containsOnly(token, "\\)")) { + /* + * Handle groups of parenthesis for multiple nesting levels + */ + final String swappedToken = token.replace(')', '('); + + /* + * Remove tokens up to a matching parenthesis + */ + while (!stack.peek().equals(swappedToken)) { + output.add(transformer.apply(stack.pop())); + } + + /* + * Remove the parenthesis + */ + stack.pop(); + } else { + /* + * Just add the transformed token + */ + output.add(transformer.apply(token)); + } + } + } + + /* + * Holds all the shuntable operations. + */ + private IMap<String, IPrecedent> operators; + + /** + * Create a new shunting yard with a default set of operators. + * + * @param configureBasics + * Whether or not basic math operators should be + * provided. + */ + public ShuntingYard(final boolean configureBasics) { + operators = new FunctionalMap<>(); + + /* + * Add basic operators if we're configured to do so + */ + if (configureBasics) { + operators.put("+", Operator.ADD); + operators.put("-", Operator.SUBTRACT); + operators.put("*", Operator.MULTIPLY); + operators.put("/", Operator.DIVIDE); + } + } + + /** + * Add an operator to the list of shuntable operators. + * + * @param operator + * The token representing the operator. + * + * @param precedence + * The precedence of the operator to add. + */ + public void addOp(final String operator, final int precedence) { + /* + * Create the precedence marker + */ + final IPrecedent prec = IPrecedent.newSimplePrecedent(precedence); + + this.addOp(operator, prec); + } + + /** + * Add an operator to the list of shuntable operators. + * + * @param operator + * The token representing the operator. + * + * @param precedence + * The precedence of the operator. + */ + public void addOp(final String operator, final IPrecedent precedence) { + /* + * Complain about trying to add an incorrect operator + */ + if (operator == null) + throw new NullPointerException("Operator must not be null"); + else if (precedence == null) throw new NullPointerException("Precedence must not be null"); + + /* + * Add the operator to the ones we handle + */ + operators.put(operator, precedence); + } + + private boolean isHigherPrec(final String left, final String right) { + /* + * Check if the right operator exists + */ + final boolean exists = operators.containsKey(right); + + /* + * If it doesn't, the left is higher precedence. + */ + if (!exists) return false; + + /* + * Get the precedence of operators + */ + final int rightPrecedence = operators.get(right).getPrecedence(); + final int leftPrecedence = operators.get(left).getPrecedence(); + + /* + * Evaluate what we were asked + */ + return rightPrecedence >= leftPrecedence; + } + + /** + * Transform a string of tokens from infix notation to postfix. + * + * @param input + * The string to transform. + * + * @param transformer + * The function to use to transform strings to tokens. + * + * @return A list of tokens in postfix notation. + */ + public IList<TokenType> postfix(final IList<String> input, final Function<String, TokenType> transformer) { + /* + * Check our input + */ + if (input == null) + throw new NullPointerException("Input must not be null"); + else if (transformer == null) throw new NullPointerException("Transformer must not be null"); + + /* + * Here's what we're handing back + */ + final IList<TokenType> output = new FunctionalList<>(); + + /* + * The stack to put operators on + */ + final Deque<String> stack = new LinkedList<>(); + + /* + * Shunt the tokens + */ + input.forEach(new TokenShunter(output, stack, transformer)); + + /* + * Transform any resulting tokens + */ + stack.forEach(token -> { + output.add(transformer.apply(token)); + }); + + return output; + } + + /** + * Remove an operator from the list of shuntable operators. + * + * @param operator + * The token representing the operator. If null, remove + * all operators. + */ + public void removeOp(final String operator) { + /* + * Check if we want to remove all operators + */ + if (operator == null) { + operators = new FunctionalMap<>(); + } else { + operators.remove(operator); + } + } +} diff --git a/base/src/main/java/bjc/utils/parserutils/StringDescaper.java b/base/src/main/java/bjc/utils/parserutils/StringDescaper.java new file mode 100644 index 0000000..096656a --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/StringDescaper.java @@ -0,0 +1,242 @@ +package bjc.utils.parserutils; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.UnaryOperator; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import static java.util.Map.Entry; + +import static bjc.utils.PropertyDB.applyFormat; +import static bjc.utils.PropertyDB.getCompiledRegex; +import static bjc.utils.PropertyDB.getRegex; + +public class StringDescaper { + private Logger LOGGER = Logger.getLogger(StringDescaper.class.getName()); + + /* + * Patterns and pattern parts. + */ + private static String rPossibleEscapeString = getRegex("possibleStringEscape"); + private static Pattern possibleEscapePatt = Pattern.compile(rPossibleEscapeString); + + private static String rShortEscape = getRegex("shortFormStringEscape"); + private static String rOctalEscape = getRegex("octalStringEscape"); + private static String rUnicodeEscape = getRegex("unicodeStringEscape"); + + private String rEscapeString; + private Pattern escapePatt; + + private static String rDoubleQuoteString = applyFormat("doubleQuotes", getRegex("nonStringEscape"), rPossibleEscapeString); + private static Pattern doubleQuotePatt = Pattern.compile(rDoubleQuoteString); + + private static Pattern quotePatt = getCompiledRegex("unescapedQuote"); + + private Map<String, String> literalEscapes; + private Map<Pattern, UnaryOperator<String>> specialEscapes; + + public StringDescaper() { + literalEscapes = new HashMap<>(); + specialEscapes = new HashMap<>(); + + rEscapeString = String.format("\\\\(%1$s|%2$s|%3$s)"); + escapePatt = Pattern.compile(rEscapeString); + } + + public void addLiteralEscape(String escape, String val) { + if(literalEscapes.containsKey(escape)) { + LOGGER.warning(String.format("Shadowing literal escape '%s'\n", escape)); + } + + literalEscapes.put(escape, val); + } + + public void addSpecialEscape(String escape, UnaryOperator<String> val) { + if(specialEscapes.containsKey(escape)) { + LOGGER.warning(String.format("Shadowing special escape '%s'\n", escape)); + } + + /* + * Make sure this special escape is a valid regex. + */ + + Pattern patt = null; + try { + patt = Pattern.compile(escape); + } catch (PatternSyntaxException psex) { + String msg = String.format("Invalid special escape '%s'", escape); + + IllegalArgumentException iaex = new IllegalArgumentException(msg); + iaex.initCause(psex); + + throw psex; + } + + specialEscapes.put(patt, val); + } + + public void compileEscapes() { + StringBuilder work = new StringBuilder(); + + for(String litEscape : literalEscapes.keySet()) { + work.append("|(?:"); + work.append(Pattern.quote(litEscape)); + work.append(")"); + } + + for(Pattern specEscape : specialEscapes.keySet()) { + work.append("|(?:"); + work.append(specEscape.toString()); + work.append(")"); + } + + /* + * Convert user-defined escapes to a regex for matching. + * We don't need a bar before %4 because the string has it. + */ + rEscapeString = String.format("\\(%1$s|%2$s|%3$s%4$s)", rShortEscape, rOctalEscape, rUnicodeEscape, work.toString()); + escapePatt = Pattern.compile(rEscapeString); + } + + /** + * Replace escape characters with their actual equivalents. + * + * @param inp + * The string to replace escape sequences in. + * + * @return The string with escape sequences replaced by their equivalent + * characters. + */ + public String descapeString(final String inp) { + if (inp == null) { + throw new NullPointerException("Input to descapeString must not be null"); + } + + /* + * Prepare the buffer and escape finder. + */ + final StringBuffer work = new StringBuffer(); + final Matcher possibleEscapeFinder = possibleEscapePatt.matcher(inp); + final Matcher escapeFinder = escapePatt.matcher(inp); + + while (possibleEscapeFinder.find()) { + if (!escapeFinder.find()) { + /* + * Found a possible escape that isn't actually an + * escape. + */ + final String msg = String.format("Illegal escape sequence '%s' at position %d of string '%s'", + possibleEscapeFinder.group(), possibleEscapeFinder.start(), inp); + throw new IllegalArgumentException(msg); + } + + final String escapeSeq = escapeFinder.group(); + + /* + * Convert the escape to a string. + */ + String escapeRep = ""; + switch (escapeSeq) { + case "\\b": + escapeRep = "\b"; + break; + case "\\t": + escapeRep = "\t"; + break; + case "\\n": + escapeRep = "\n"; + break; + case "\\f": + escapeRep = "\f"; + break; + case "\\r": + escapeRep = "\r"; + break; + case "\\\"": + escapeRep = "\""; + break; + case "\\'": + escapeRep = "'"; + break; + case "\\\\": + /* + * Skip past the second slash. + */ + possibleEscapeFinder.find(); + escapeRep = "\\"; + break; + default: + if (escapeSeq.startsWith("u")) { + escapeRep = handleUnicodeEscape(escapeSeq.substring(1)); + } else if(escapeSeq.startsWith("O")) { + escapeRep = handleOctalEscape(escapeSeq.substring(1)); + } else if(literalEscapes.containsKey(escapeSeq)) { + escapeRep = literalEscapes.get(escapeSeq); + } else { + for(Entry<Pattern, UnaryOperator<String>> ent : specialEscapes.entrySet()) { + Pattern pat = ent.getKey(); + + Matcher mat = pat.matcher(escapeSeq); + if(mat.matches()) { + escapeRep = ent.getValue().apply(escapeSeq); + break; + } + } + } + } + + escapeFinder.appendReplacement(work, escapeRep); + } + + escapeFinder.appendTail(work); + + return work.toString(); + } + + /* + * Handle a unicode codepoint. + */ + private static String handleUnicodeEscape(final String seq) { + try { + final int codepoint = Integer.parseInt(seq, 16); + + return new String(Character.toChars(codepoint)); + } catch (final IllegalArgumentException iaex) { + final String msg = String.format("'%s' is not a valid Unicode escape sequence'", seq); + + final IllegalArgumentException reiaex = new IllegalArgumentException(msg); + + reiaex.initCause(iaex); + + throw reiaex; + } + } + + /* + * Handle a octal codepoint. + */ + private static String handleOctalEscape(final String seq) { + try { + final int codepoint = Integer.parseInt(seq, 8); + + if (codepoint > 255) { + final String msg = String.format("'%d' is outside the range of octal escapes', codepoint"); + + throw new IllegalArgumentException(msg); + } + + return new String(Character.toChars(codepoint)); + } catch (final IllegalArgumentException iaex) { + final String msg = String.format("'%s' is not a valid octal escape sequence'", seq); + + final IllegalArgumentException reiaex = new IllegalArgumentException(msg); + + reiaex.initCause(iaex); + + throw reiaex; + } + } +} diff --git a/base/src/main/java/bjc/utils/parserutils/TokenTransformer.java b/base/src/main/java/bjc/utils/parserutils/TokenTransformer.java new file mode 100644 index 0000000..30ccc5a --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/TokenTransformer.java @@ -0,0 +1,131 @@ +package bjc.utils.parserutils; + +import java.util.Deque; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +import bjc.utils.data.IHolder; +import bjc.utils.data.ITree; +import bjc.utils.data.Pair; +import bjc.utils.data.Tree; +import bjc.utils.parserutils.TreeConstructor.ConstructorState; +import bjc.utils.parserutils.TreeConstructor.QueueFlattener; + +/* + * Handle creating ASTs from tokens. + */ +final class TokenTransformer<TokenType> implements Consumer<TokenType> { + /* + * Handle operators + */ + private final class OperatorHandler implements UnaryOperator<ConstructorState<TokenType>> { + private final TokenType element; + + public OperatorHandler(final TokenType element) { + this.element = element; + } + + @Override + public ConstructorState<TokenType> apply(final ConstructorState<TokenType> pair) { + /* + * Replace the current AST with the result of handling an operator + */ + return new ConstructorState<>(pair.bindLeft(queuedASTs -> { + return handleOperator(queuedASTs); + })); + } + + private ConstructorState<TokenType> handleOperator(final Deque<ITree<TokenType>> queuedASTs) { + /* + * The AST we're going to hand back + */ + ITree<TokenType> newAST; + + /* + * Handle special operators + */ + if (isSpecialOperator.test(element)) { + newAST = handleSpecialOperator.apply(element).apply(queuedASTs); + } else { + /* + * Error if we don't have enough for a binary operator + */ + if (queuedASTs.size() < 2) { + final String msg = String.format( + "Attempted to parse binary operator without enough operands\n\tProblem operator is: %s\n\tPossible operand is: %s", + element.toString(), queuedASTs.peek().toString()); + + throw new IllegalStateException(msg); + } + + /* + * Grab the two operands + */ + final ITree<TokenType> right = queuedASTs.pop(); + final ITree<TokenType> left = queuedASTs.pop(); + + /* + * Create a new AST + */ + newAST = new Tree<>(element, left, right); + } + + /* + * Stick it onto the stack + */ + queuedASTs.push(newAST); + + /* + * Hand back the state + */ + return new ConstructorState<>(queuedASTs, newAST); + } + } + + private final IHolder<ConstructorState<TokenType>> initialState; + + private final Predicate<TokenType> operatorPredicate; + + private final Predicate<TokenType> isSpecialOperator; + private final Function<TokenType, QueueFlattener<TokenType>> handleSpecialOperator; + + /* + * Create a new transformer + */ + public TokenTransformer(final IHolder<ConstructorState<TokenType>> initialState, + final Predicate<TokenType> operatorPredicate, final Predicate<TokenType> isSpecialOperator, + final Function<TokenType, QueueFlattener<TokenType>> handleSpecialOperator) { + this.initialState = initialState; + this.operatorPredicate = operatorPredicate; + this.isSpecialOperator = isSpecialOperator; + this.handleSpecialOperator = handleSpecialOperator; + } + + @Override + public void accept(final TokenType element) { + /* + * Handle operators + */ + if (operatorPredicate.test(element)) { + initialState.transform(new OperatorHandler(element)); + } else { + final ITree<TokenType> newAST = new Tree<>(element); + + /* + * Insert the new tree into the AST + */ + initialState.transform(pair -> { + /* + * Transform the pair, ignoring the current AST in favor of the one consisting of the current element + */ + return new ConstructorState<>(pair.bindLeft(queue -> { + queue.push(newAST); + + return new Pair<>(queue, newAST); + })); + }); + } + } +} diff --git a/base/src/main/java/bjc/utils/parserutils/TokenUtils.java b/base/src/main/java/bjc/utils/parserutils/TokenUtils.java new file mode 100644 index 0000000..67c1e5a --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/TokenUtils.java @@ -0,0 +1,303 @@ +package bjc.utils.parserutils; + +import static bjc.utils.PropertyDB.applyFormat; +import static bjc.utils.PropertyDB.getCompiledRegex; +import static bjc.utils.PropertyDB.getRegex; + +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.IList; +import bjc.utils.parserutils.splitter.TokenSplitter; + +/** + * Utilities useful for operating on PL tokens. + * + * @author EVE + * + */ +public class TokenUtils { + /** + * Simple implementation of TokenSplitter for removing double-quoted + * strings. + * + * @author EVE + * + */ + public static class StringTokenSplitter implements TokenSplitter { + @Override + public IList<String> split(final String input) { + return new FunctionalList<>(TokenUtils.removeDQuotedStrings(input)); + } + } + + /* + * Patterns and pattern parts. + */ + private static String rPossibleEscapeString = getRegex("possibleStringEscape"); + + private static Pattern possibleEscapePatt = Pattern.compile(rPossibleEscapeString); + + private static String rShortEscape = getRegex("shortFormStringEscape"); + private static String rOctalEscape = getRegex("octalStringEscape"); + private static String rUnicodeEscape = getRegex("unicodeStringEscape"); + + private static String rEscapeString = applyFormat("stringEscape", rShortEscape, rOctalEscape, rUnicodeEscape); + + private static Pattern escapePatt = Pattern.compile(rEscapeString); + + private static String rDoubleQuoteString = applyFormat("doubleQuotes", getRegex("nonStringEscape"), + rPossibleEscapeString); + + private static Pattern doubleQuotePatt = Pattern.compile(rDoubleQuoteString); + + private static Pattern quotePatt = getCompiledRegex("unescapedQuote"); + + private static Pattern intLitPattern = getCompiledRegex("intLiteral"); + + /** + * Remove double quoted strings from a string. + * + * Splits a string around instances of java-style double-quoted strings. + * + * @param inp + * The string to split. + * + * @return An list containing alternating bits of the string and the + * embedded double-quoted strings that separated them. + */ + public static List<String> removeDQuotedStrings(final String inp) { + if (inp == null) throw new NullPointerException("inp must not be null"); + + /* + * What we need for piece-by-piece string building + */ + StringBuffer work = new StringBuffer(); + final List<String> res = new LinkedList<>(); + + /* + * Matcher for proper strings and single quotes. + */ + final Matcher mt = doubleQuotePatt.matcher(inp); + final Matcher corr = quotePatt.matcher(inp); + + if (corr.find() && !corr.find()) { + /* + * There's a unmatched opening quote with no strings. + */ + final String msg = String.format( + "Unclosed string literal '%s'. Opening quote was at position %d", inp, + inp.indexOf("\"")); + + throw new IllegalArgumentException(msg); + } + + while (mt.find()) { + /* + * Remove the string until the quoted string. + */ + mt.appendReplacement(work, ""); + + /* + * Add the string preceding the double-quoted string and + * the double-quoted string to the list. + */ + res.add(work.toString()); + res.add(mt.group(1)); + + /* + * Renew the buffer. + */ + work = new StringBuffer(); + } + + /* + * Grab the remainder of the string. + */ + mt.appendTail(work); + final String tail = work.toString(); + + if (tail.contains("\"")) { + /* + * There's a unmatched opening quote with at least one + * string. + */ + final String msg = String.format( + "Unclosed string literal '%s'. Opening quote was at position %d", inp, + inp.lastIndexOf("\"")); + + throw new IllegalArgumentException(msg); + } + + /* + * Only add an empty tail if the string was empty. + */ + if (!tail.equals("") || res.isEmpty()) { + res.add(tail); + } + + return res; + } + + /** + * Replace escape characters with their actual equivalents. + * + * @param inp + * The string to replace escape sequences in. + * + * @return The string with escape sequences replaced by their equivalent + * characters. + */ + public static String descapeString(final String inp) { + if (inp == null) throw new NullPointerException("inp must not be null"); + + /* + * Prepare the buffer and escape finder. + */ + final StringBuffer work = new StringBuffer(); + final Matcher possibleEscapeFinder = possibleEscapePatt.matcher(inp); + final Matcher escapeFinder = escapePatt.matcher(inp); + + while (possibleEscapeFinder.find()) { + if (!escapeFinder.find()) { + /* + * Found a possible escape that isn't actually an + * escape. + */ + final String msg = String.format("Illegal escape sequence '%s' at position %d", + possibleEscapeFinder.group(), possibleEscapeFinder.start()); + + throw new IllegalArgumentException(msg); + } + + final String escapeSeq = escapeFinder.group(); + + /* + * Convert the escape to a string. + */ + String escapeRep = ""; + switch (escapeSeq) { + case "\\b": + escapeRep = "\b"; + break; + case "\\t": + escapeRep = "\t"; + break; + case "\\n": + escapeRep = "\n"; + break; + case "\\f": + escapeRep = "\f"; + break; + case "\\r": + escapeRep = "\r"; + break; + case "\\\"": + escapeRep = "\""; + break; + case "\\'": + escapeRep = "'"; + break; + case "\\\\": + /* + * Skip past the second slash. + */ + possibleEscapeFinder.find(); + escapeRep = "\\"; + break; + default: + if (escapeSeq.startsWith("u")) { + escapeRep = handleUnicodeEscape(escapeSeq.substring(1)); + } else { + escapeRep = handleOctalEscape(escapeSeq); + } + } + + escapeFinder.appendReplacement(work, escapeRep); + } + + escapeFinder.appendTail(work); + + return work.toString(); + } + + /* + * Handle a unicode codepoint. + */ + private static String handleUnicodeEscape(final String seq) { + try { + final int codepoint = Integer.parseInt(seq, 16); + + return new String(Character.toChars(codepoint)); + } catch (final IllegalArgumentException iaex) { + final String msg = String.format("'%s' is not a valid Unicode escape sequence'", seq); + + final IllegalArgumentException reiaex = new IllegalArgumentException(msg); + + reiaex.initCause(iaex); + + throw reiaex; + } + } + + /* + * Handle a octal codepoint. + */ + private static String handleOctalEscape(final String seq) { + try { + final int codepoint = Integer.parseInt(seq, 8); + + if (codepoint > 255) { + final String msg = String + .format("'%d' is outside the range of octal escapes', codepoint"); + + throw new IllegalArgumentException(msg); + } + + return new String(Character.toChars(codepoint)); + } catch (final IllegalArgumentException iaex) { + final String msg = String.format("'%s' is not a valid octal escape sequence'", seq); + + final IllegalArgumentException reiaex = new IllegalArgumentException(msg); + + reiaex.initCause(iaex); + + throw reiaex; + } + } + + /** + * Check if a given string would be successfully converted to a double + * by {@link Double#parseDouble(String)}. + * + * @param inp + * The string to check. + * @return Whether the string is a valid double or not. + */ + public static boolean isDouble(final String inp) { + return DoubleMatcher.doubleLiteral.matcher(inp).matches(); + } + + /** + * Check if a given string would be successfully converted to a integer + * by {@link Integer#parseInt(String)}. + * + * NOTE: This only checks syntax. Using values out of the range of + * integers will still cause errors. + * + * @param inp + * The input to check. + * @return Whether the string is a valid integer or not. + */ + public static boolean isInt(final String inp) { + try { + Integer.parseInt(inp); + return true; + } catch (NumberFormatException nfex) { + return false; + } + } +} diff --git a/base/src/main/java/bjc/utils/parserutils/TreeConstructor.java b/base/src/main/java/bjc/utils/parserutils/TreeConstructor.java new file mode 100644 index 0000000..90141ef --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/TreeConstructor.java @@ -0,0 +1,125 @@ +package bjc.utils.parserutils; + +import java.util.Deque; +import java.util.LinkedList; +import java.util.function.Function; +import java.util.function.Predicate; + +import bjc.utils.data.IHolder; +import bjc.utils.data.IPair; +import bjc.utils.data.ITree; +import bjc.utils.data.Identity; +import bjc.utils.data.Pair; +import bjc.utils.funcdata.IList; + +/** + * Creates a parse tree from a postfix expression + * + * @author ben + * + */ +public class TreeConstructor { + /** + * Alias interface for special operator types. + * + * @param <TokenType> + * The token type of the tree. + */ + public interface QueueFlattener<TokenType> extends Function<Deque<ITree<TokenType>>, ITree<TokenType>> { + + } + + /* + * Alias for constructor state. + */ + static final class ConstructorState<TokenType> extends Pair<Deque<ITree<TokenType>>, ITree<TokenType>> { + public ConstructorState(final Deque<ITree<TokenType>> left, final ITree<TokenType> right) { + super(left, right); + } + + public ConstructorState(final IPair<Deque<ITree<TokenType>>, ITree<TokenType>> par) { + super(par.getLeft(), par.getRight()); + } + } + + /** + * Construct a tree from a list of tokens in postfix notation + * + * Only binary operators are accepted. + * + * @param <TokenType> + * The elements of the parse tree + * @param tokens + * The list of tokens to build a tree from + * @param isOperator + * The predicate to use to determine if something is a + * operator + * @return A AST from the expression + */ + public static <TokenType> ITree<TokenType> constructTree(final IList<TokenType> tokens, + final Predicate<TokenType> isOperator) { + /* + * Construct a tree with no special operators + */ + return constructTree(tokens, isOperator, op -> false, null); + } + + /** + * Construct a tree from a list of tokens in postfix notation. + * + * Only binary operators are accepted by default. Use the last two + * parameters to handle non-binary operators. + * + * @param <TokenType> + * The elements of the parse tree. + * + * @param tokens + * The list of tokens to build a tree from. + * + * @param isOperator + * The predicate to use to determine if something is a + * operator. + * + * @param isSpecialOperator + * The predicate to use to determine if an operator needs + * special handling. + * + * @param handleSpecialOperator + * The function to use to handle special case operators. + * + * @return A AST from the expression + * + */ + public static <TokenType> ITree<TokenType> constructTree(final IList<TokenType> tokens, + final Predicate<TokenType> isOperator, final Predicate<TokenType> isSpecialOperator, + final Function<TokenType, QueueFlattener<TokenType>> handleSpecialOperator) { + /* + * Make sure our parameters are valid + */ + if (tokens == null) + throw new NullPointerException("Tokens must not be null"); + else if (isOperator == null) + throw new NullPointerException("Operator predicate must not be null"); + else if (isSpecialOperator == null) + throw new NullPointerException("Special operator determiner must not be null"); + + /* + * Here is the state for the tree construction + */ + final IHolder<ConstructorState<TokenType>> initialState = new Identity<>( + new ConstructorState<>(new LinkedList<>(), null)); + + /* + * Transform each of the tokens + */ + tokens.forEach(new TokenTransformer<>(initialState, isOperator, isSpecialOperator, + handleSpecialOperator)); + + /* + * Grab the tree from the state + */ + return initialState.unwrap(pair -> { + return pair.getRight(); + }); + } +} diff --git a/base/src/main/java/bjc/utils/parserutils/defines/IteratedDefine.java b/base/src/main/java/bjc/utils/parserutils/defines/IteratedDefine.java new file mode 100644 index 0000000..552b471 --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/defines/IteratedDefine.java @@ -0,0 +1,48 @@ +package bjc.utils.parserutils.defines; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.function.UnaryOperator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import bjc.utils.data.CircularIterator; + +public class IteratedDefine implements UnaryOperator<String> { + private Pattern patt; + + private Iterator<String> repls; + + /** + * Create a new iterated define. + * + * @param pattern + * The pattern to use for matching. + * @param circular + * Whether or not to loop through the list of replacers, or just + * repeat the last one. + * @param replacers + * The set of replacers to use. + */ + public IteratedDefine(Pattern pattern, boolean circular, String... replacers) { + patt = pattern; + + repls = new CircularIterator<>(Arrays.asList(replacers), circular); + } + + @Override + public String apply(String ln) { + Matcher mat = patt.matcher(ln); + StringBuffer sb = new StringBuffer(); + + while(mat.find()) { + String repl = repls.next(); + + mat.appendReplacement(sb, repl); + } + + mat.appendTail(sb); + + return sb.toString(); + } +} diff --git a/base/src/main/java/bjc/utils/parserutils/defines/SimpleDefine.java b/base/src/main/java/bjc/utils/parserutils/defines/SimpleDefine.java new file mode 100644 index 0000000..42866c2 --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/defines/SimpleDefine.java @@ -0,0 +1,23 @@ +package bjc.utils.parserutils.defines; + +import java.util.function.UnaryOperator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SimpleDefine implements UnaryOperator<String> { + private Pattern patt; + private String repl; + + public SimpleDefine(Pattern pattern, String replace) { + patt = pattern; + + repl = replace; + } + + @Override + public String apply(String line) { + Matcher mat = patt.matcher(line); + + return mat.replaceAll(repl); + } +} diff --git a/base/src/main/java/bjc/utils/parserutils/delims/DelimiterException.java b/base/src/main/java/bjc/utils/parserutils/delims/DelimiterException.java new file mode 100644 index 0000000..071afb4 --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/delims/DelimiterException.java @@ -0,0 +1,21 @@ +package bjc.utils.parserutils.delims; + +/** + * The superclass for exceptions thrown during sequence delimitation. + */ +public class DelimiterException extends RuntimeException { + /** + * + */ + private static final long serialVersionUID = 2079514406049040888L; + + /** + * Create a new generic delimiter exception. + * + * @param res + * The reason for this exception. + */ + public DelimiterException(final String res) { + super(res); + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/parserutils/delims/DelimiterGroup.java b/base/src/main/java/bjc/utils/parserutils/delims/DelimiterGroup.java new file mode 100644 index 0000000..b1d8597 --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/delims/DelimiterGroup.java @@ -0,0 +1,593 @@ +package bjc.utils.parserutils.delims; + +import java.util.Arrays; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.function.Function; + +import bjc.utils.data.IPair; +import bjc.utils.data.ITree; +import bjc.utils.data.Pair; +import bjc.utils.data.Tree; +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.IList; + +/** + * Represents a possible delimiter group to match. + * + * @author EVE + * + * @param <T> + * The type of items in the sequence. + */ +public class DelimiterGroup<T> { + /** + * Represents an instance of a delimiter group. + * + * @author EVE + * + */ + public class OpenGroup { + /* + * The contents of this group. + */ + private final Deque<ITree<T>> contents; + + /* + * The contents of the current subgroup. + */ + private IList<ITree<T>> currentGroup; + + /* + * The token that opened the group, and any opening parameters. + */ + private final T opener; + private final T[] params; + + /** + * Create a new instance of a delimiter group. + * + * @param open + * The item that opened this group. + * + * @param parms + * Any parameters from the opener. + */ + public OpenGroup(final T open, final T[] parms) { + opener = open; + params = parms; + + contents = new LinkedList<>(); + + currentGroup = new FunctionalList<>(); + } + + /** + * Add an item to this group instance. + * + * @param itm + * The item to add to this group instance. + */ + public void addItem(final ITree<T> itm) { + currentGroup.add(itm); + } + + /** + * Mark a subgroup. + * + * @param marker + * The item that indicated this subgroup. + * + * @param chars + * The characteristics for building the tree. + */ + public void markSubgroup(final T marker, final SequenceCharacteristics<T> chars) { + /* + * Add all of the contents to the subgroup. + */ + final ITree<T> subgroupContents = new Tree<>(chars.contents); + for (final ITree<T> itm : currentGroup) { + subgroupContents.addChild(itm); + } + + /* + * Handle subordinate sub-groups. + */ + while (!contents.isEmpty()) { + final ITree<T> possibleSubordinate = contents.peek(); + + /* + * Subordinate lower priority subgroups. + */ + if (possibleSubordinate.getHead().equals(chars.subgroup)) { + final T otherMarker = possibleSubordinate.getChild(1).getHead(); + + if (subgroups.get(marker) > subgroups.get(otherMarker)) { + subgroupContents.prependChild(contents.pop()); + } else { + break; + } + } else { + subgroupContents.prependChild(contents.pop()); + } + } + + final Tree<T> subgroup = new Tree<>(chars.subgroup, subgroupContents, new Tree<>(marker)); + + contents.push(subgroup); + + currentGroup = new FunctionalList<>(); + } + + /** + * Convert this group into a tree. + * + * @param closer + * The item that closed this group. + * + * @param chars + * The characteristics for building the tree. + * + * @return This group as a tree. + */ + public ITree<T> toTree(final T closer, final SequenceCharacteristics<T> chars) { + /* + * Mark any implied subgroups. + */ + if (impliedSubgroups.containsKey(closer)) { + markSubgroup(impliedSubgroups.get(closer), chars); + } + + final ITree<T> res = new Tree<>(chars.contents); + + /* + * Add either the contents of the current group, + * or subgroups if they're their. + */ + if (contents.isEmpty()) { + currentGroup.forEach(res::addChild); + } else { + while (!contents.isEmpty()) { + res.prependChild(contents.poll()); + } + + currentGroup.forEach(res::addChild); + } + + return new Tree<>(groupName, new Tree<>(opener), res, new Tree<>(closer)); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + + builder.append("OpenGroup [contents="); + builder.append(contents); + builder.append(", currentGroup="); + builder.append(currentGroup); + builder.append(", opener="); + builder.append(opener); + builder.append("]"); + + return builder.toString(); + } + + /** + * Check if a group is excluded at the top level of this group. + * + * @param groupName + * The group to check. + * + * @return Whether or not the provided group is excluded. + */ + public boolean excludes(final T groupName) { + return topLevelExclusions.contains(groupName); + } + + /** + * Check if the provided delimiter would close this group. + * + * @param del + * The string to check as a closing delimiter. + * + * @return Whether or not the provided delimiter closes this + * group. + */ + public boolean isClosing(final T del) { + if (closingDelimiters.contains(del)) return true; + + for (final BiPredicate<T, T[]> pred : predClosers) { + if (pred.test(del, params)) return true; + } + + return closingDelimiters.contains(del); + } + + /** + * Get the name of the group this is an instance of. + * + * @return The name of the group this is an instance of. + */ + public T getName() { + return groupName; + } + + /** + * Get the groups that aren't allowed at all in this group. + * + * @return The groups that aren't allowed at all in this group. + */ + public Set<T> getNestingExclusions() { + return groupExclusions; + } + + /** + * Get the groups that are allowed to open anywhere inside this + * group. + * + * @return The groups allowed to open anywhere inside this + * group. + */ + public Map<T, T> getNestingOpeners() { + return nestedOpenDelimiters; + } + + /** + * Checks if a given token marks a subgroup. + * + * @param tok + * The token to check. + * + * @return Whether or not the token marks a subgroup. + */ + public boolean marksSubgroup(final T tok) { + return subgroups.containsKey(tok); + } + + /** + * Checks if a given token opens a group. + * + * @param marker + * The token to check. + * + * @return The name of the group T opens, or null if it doesn't + * open one. + */ + public IPair<T, T[]> doesOpen(final T marker) { + if (openDelimiters.containsKey(marker)) return new Pair<>(openDelimiters.get(marker), null); + + for (final Function<T, IPair<T, T[]>> pred : predOpeners) { + final IPair<T, T[]> par = pred.apply(marker); + + if (par.getLeft() != null) return par; + } + + return new Pair<>(null, null); + } + + /** + * Check if this group starts a new nesting scope. + * + * @return Whether this group starts a new nesting scope. + */ + public boolean isForgetful() { + return forgetful; + } + } + + /** + * The name of this delimiter group. + */ + public final T groupName; + + /* + * The delimiters that open groups at the top level of this group. + */ + private final Map<T, T> openDelimiters; + + /* + * The delimiters that open groups inside of this group. + */ + private final Map<T, T> nestedOpenDelimiters; + + /* + * The delimiters that close this group. + */ + private final Set<T> closingDelimiters; + + /* + * The groups that can't occur in the top level of this group. + */ + private final Set<T> topLevelExclusions; + + /* + * The groups that can't occur anywhere inside this group. + */ + private final Set<T> groupExclusions; + + /* + * Mapping from sub-group delimiters, to any sub-groups enclosed in + * them. + */ + private final Map<T, Integer> subgroups; + + /* + * Subgroups implied by a particular closing delimiter + */ + private final Map<T, T> impliedSubgroups; + + /* + * Allows more complex openings + */ + private final List<Function<T, IPair<T, T[]>>> predOpeners; + + /* + * Allow more complex closings + */ + private final List<BiPredicate<T, T[]>> predClosers; + + /* + * Whether or not this group starts a new nesting set. + */ + private boolean forgetful; + + /** + * Create a new empty delimiter group. + * + * @param name + * The name of the delimiter group + */ + public DelimiterGroup(final T name) { + if (name == null) throw new NullPointerException("Group name must not be null"); + + groupName = name; + + openDelimiters = new HashMap<>(); + nestedOpenDelimiters = new HashMap<>(); + + closingDelimiters = new HashSet<>(); + + topLevelExclusions = new HashSet<>(); + groupExclusions = new HashSet<>(); + + subgroups = new HashMap<>(); + impliedSubgroups = new HashMap<>(); + + predOpeners = new LinkedList<>(); + predClosers = new LinkedList<>(); + } + + /** + * Adds one or more delimiters that close this group. + * + * @param closers + * Delimiters that close this group. + */ + @SafeVarargs + public final void addClosing(final T... closers) { + final List<T> closerList = Arrays.asList(closers); + + for (final T closer : closerList) { + if (closer == null) + throw new NullPointerException("Closing delimiter must not be null"); + else if (closer.equals("")) + /* + * We can do this because equals works on + * arbitrary objects, not just those of the same + * type. + */ + throw new IllegalArgumentException("Empty string is not a valid exclusion"); + else { + closingDelimiters.add(closer); + } + } + } + + /** + * Adds one or more groups that cannot occur in the top level of this + * group. + * + * @param exclusions + * The groups forbidden in the top level of this group. + */ + @SafeVarargs + public final void addTopLevelForbid(final T... exclusions) { + for (final T exclusion : exclusions) { + if (exclusion == null) + throw new NullPointerException("Exclusion must not be null"); + else if (exclusion.equals("")) + /* + * We can do this because equals works on + * arbitrary objects, not just those of the same + * type. + */ + throw new IllegalArgumentException("Empty string is not a valid exclusion"); + else { + topLevelExclusions.add(exclusion); + } + } + } + + /** + * Adds one or more groups that cannot occur at all in this group. + * + * @param exclusions + * The groups forbidden inside this group. + */ + @SafeVarargs + public final void addGroupForbid(final T... exclusions) { + for (final T exclusion : exclusions) { + if (exclusion == null) + throw new NullPointerException("Exclusion must not be null"); + else if (exclusion.equals("")) + /* + * We can do this because equals works on + * arbitrary objects, not just those of the same + * type. + */ + throw new IllegalArgumentException("Empty string is not a valid exclusion"); + else { + groupExclusions.add(exclusion); + } + } + } + + /** + * Adds sub-group markers to this group. + * + * @param subgroup + * The token to mark a sub-group. + * + * @param priority + * The priority of this sub-group. + */ + public void addSubgroup(final T subgroup, final int priority) { + if (subgroup == null) throw new NullPointerException("Subgroup marker must not be null"); + + subgroups.put(subgroup, priority); + } + + /** + * Adds a marker that opens a group at the top level of this group. + * + * @param opener + * The marker that opens the group. + * + * @param group + * The group opened by the marker. + */ + public void addOpener(final T opener, final T group) { + if (opener == null) throw new NullPointerException("Opener must not be null"); + else if (group == null) throw new NullPointerException("Group to open must not be null"); + + openDelimiters.put(opener, group); + } + + /** + * Adds a marker that opens a group inside of this group. + * + * @param opener + * The marker that opens the group. + * + * @param group + * The group opened by the marker. + */ + public void addNestedOpener(final T opener, final T group) { + if (opener == null) throw new NullPointerException("Opener must not be null"); + else if (group == null) throw new NullPointerException("Group to open must not be null"); + + nestedOpenDelimiters.put(opener, group); + } + + /** + * Mark a closing delimiter as implying a subgroup. + * + * @param closer + * The closing delimiter. + * + * @param subgroup + * The subgroup to imply. + */ + public void implySubgroup(final T closer, final T subgroup) { + if (closer == null) throw new NullPointerException("Closer must not be null"); + else if (subgroup == null) throw new NullPointerException("Subgroup must not be null"); + else if (!closingDelimiters.contains(closer)) throw new IllegalArgumentException(String.format("No closing delimiter '%s' defined", closer)); + else if (!subgroups.containsKey(subgroup)) throw new IllegalArgumentException(String.format("No subgroup '%s' defined", subgroup)); + + impliedSubgroups.put(closer, subgroup); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + + builder.append("("); + + builder.append("groupName=["); + builder.append(groupName); + builder.append("], "); + + builder.append("closingDelimiters=["); + for (final T closer : closingDelimiters) { + builder.append(closer + ","); + } + builder.deleteCharAt(builder.length() - 1); + builder.append("]"); + + if (topLevelExclusions != null && !topLevelExclusions.isEmpty()) { + builder.append(", "); + builder.append("topLevelExclusions=["); + for (final T exclusion : topLevelExclusions) { + builder.append(exclusion + ","); + } + builder.deleteCharAt(builder.length() - 1); + builder.append("]"); + } + + if (groupExclusions != null && !groupExclusions.isEmpty()) { + builder.append(", "); + builder.append("groupExclusions=["); + for (final T exclusion : groupExclusions) { + builder.append(exclusion + ","); + } + builder.deleteCharAt(builder.length() - 1); + builder.append("]"); + } + + builder.append(" )"); + + return builder.toString(); + } + + /** + * Open an instance of this group. + * + * @param opener + * The item that opened this group. + * + * @param parms + * The parameters that opened this group + * + * @return An opened instance of this group. + */ + public OpenGroup open(final T opener, final T[] parms) { + return new OpenGroup(opener, parms); + } + + /** + * Adds a predicated opener to the top level of this group. + * + * @param pred + * The predicate that defines the opener and its + * parameters. + */ + public void addPredOpener(final Function<T, IPair<T, T[]>> pred) { + predOpeners.add(pred); + } + + /** + * Adds a predicated closer to the top level of this group. + * + * @param pred + * The predicate that defines the closer. + */ + public void addPredCloser(final BiPredicate<T, T[]> pred) { + predClosers.add(pred); + } + + /** + * Set whether or not this group starts a new nesting set. + * + * @param forgetful + * Whether this group starts a new nesting set. + */ + public void setForgetful(final boolean forgetful) { + this.forgetful = forgetful; + } +} diff --git a/base/src/main/java/bjc/utils/parserutils/delims/RegexCloser.java b/base/src/main/java/bjc/utils/parserutils/delims/RegexCloser.java new file mode 100644 index 0000000..4b29949 --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/delims/RegexCloser.java @@ -0,0 +1,33 @@ +package bjc.utils.parserutils.delims; + +import java.util.function.BiPredicate; + +/** + * A predicated closer for use with {@link RegexOpener}. + * + * @author bjculkin + * + */ +public class RegexCloser implements BiPredicate<String, String[]> { + private final String rep; + + /** + * Create a new regex closer. + * + * @param closer + * The format string to use for closing. + */ + public RegexCloser(final String closer) { + rep = closer; + } + + @Override + public boolean test(final String closer, final String[] params) { + /* + * Confirm passing an array instead of a single var-arg. + */ + final String work = String.format(rep, (Object[]) params); + + return work.equals(closer); + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/parserutils/delims/RegexOpener.java b/base/src/main/java/bjc/utils/parserutils/delims/RegexOpener.java new file mode 100644 index 0000000..ee93b73 --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/delims/RegexOpener.java @@ -0,0 +1,54 @@ +package bjc.utils.parserutils.delims; + +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import bjc.utils.data.IPair; +import bjc.utils.data.Pair; + +/** + * A predicated opener for use with {@link RegexCloser} + * + * @author bjculkin + * + */ +public class RegexOpener implements Function<String, IPair<String, String[]>> { + private final String name; + + private final Pattern patt; + + /** + * Create a new regex opener. + * + * @param groupName + * The name of the opened group. + * + * @param groupRegex + * The regex that matches the opener. + */ + public RegexOpener(final String groupName, final String groupRegex) { + name = groupName; + + patt = Pattern.compile(groupRegex); + } + + @Override + public IPair<String, String[]> apply(final String str) { + final Matcher m = patt.matcher(str); + + if (m.matches()) { + final int numGroups = m.groupCount(); + + final String[] parms = new String[numGroups + 1]; + + for (int i = 0; i <= numGroups; i++) { + parms[i] = m.group(i); + } + + return new Pair<>(name, parms); + } + + return new Pair<>(null, null); + } +} diff --git a/base/src/main/java/bjc/utils/parserutils/delims/SequenceCharacteristics.java b/base/src/main/java/bjc/utils/parserutils/delims/SequenceCharacteristics.java new file mode 100644 index 0000000..882b4c5 --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/delims/SequenceCharacteristics.java @@ -0,0 +1,93 @@ +package bjc.utils.parserutils.delims; + +/** + * Marks the parameters for building a sequence tree. + * + * @author EVE + * + * @param <T> + * The type of item in the tree. + */ +public class SequenceCharacteristics<T> { + /** + * The item to mark the root of the tree. + */ + public final T root; + + /** + * The item to mark the contents of a group/subgroup. + */ + + public final T contents; + + /** + * The item to mark a subgroup. + */ + public final T subgroup; + + /** + * Create a new set of parameters for building a tree. + * + * @param root + * The root marker. + * @param contents + * The group/subgroup contents marker. + * @param subgroup + * The subgroup marker. + */ + public SequenceCharacteristics(final T root, final T contents, final T subgroup) { + this.root = root; + this.contents = contents; + this.subgroup = subgroup; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + + result = prime * result + (contents == null ? 0 : contents.hashCode()); + result = prime * result + (root == null ? 0 : root.hashCode()); + result = prime * result + (subgroup == null ? 0 : subgroup.hashCode()); + + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof SequenceCharacteristics)) return false; + + final SequenceCharacteristics<?> other = (SequenceCharacteristics<?>) obj; + + if (contents == null) { + if (other.contents != null) return false; + } else if (!contents.equals(other.contents)) return false; + + if (root == null) { + if (other.root != null) return false; + } else if (!root.equals(other.root)) return false; + + if (subgroup == null) { + if (other.subgroup != null) return false; + } else if (!subgroup.equals(other.subgroup)) return false; + + return true; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + + builder.append("SequenceCharacteristics [root="); + builder.append(root == null ? "(null)" : root); + builder.append(", contents="); + builder.append(contents == null ? "(null)" : contents); + builder.append(", subgroup="); + builder.append(subgroup == null ? "(null)" : subgroup); + builder.append("]"); + + return builder.toString(); + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/parserutils/delims/SequenceDelimiter.java b/base/src/main/java/bjc/utils/parserutils/delims/SequenceDelimiter.java new file mode 100644 index 0000000..ccfaffb --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/delims/SequenceDelimiter.java @@ -0,0 +1,371 @@ +package bjc.utils.parserutils.delims; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.HashMultiset; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multiset; + +import bjc.utils.data.IPair; +import bjc.utils.data.ITree; +import bjc.utils.data.Tree; +import bjc.utils.esodata.PushdownMap; +import bjc.utils.esodata.SimpleStack; +import bjc.utils.esodata.Stack; +import bjc.utils.funcdata.IMap; +import bjc.utils.funcutils.StringUtils; + +/** + * Convert linear sequences into trees that represent group structure. + * + * @author EVE + * + * @param <T> + * The type of items in the sequence. + */ +public class SequenceDelimiter<T> { + /* + * Mapping from group names to actual groups. + */ + private final Map<T, DelimiterGroup<T>> groups; + + /* + * The initial group to start with. + */ + private DelimiterGroup<T> initialGroup; + + /** + * Create a new sequence delimiter. + */ + public SequenceDelimiter() { + groups = new HashMap<>(); + } + + /** + * Convert a linear sequence into a tree that matches the delimiter + * structure. + * + * Essentially, creates a parse tree of the expression against the + * following grammar while obeying the defined grouping rules. + * + * <pre> + * <tree> → (<data> | <subgroup> | <group>)* + * <subgroup> → <tree> <marker> + * <group> → <open> <tree> <close> + * + * <data> → STRING + * <open> → STRING + * <close> → STRING + * <marker> → STRING + * </pre> + * + * @param chars + * The parameters on how to mark certain portions of the + * tree. + * @param seq + * The sequence to delimit. + * + * @return The sequence as a tree that matches its group structure. Each + * node in the tree is either a data node, a subgroup node, or a + * group node. + * + * A data node is a leaf node whose data is the string it + * represents. + * + * A subgroup node is a node with two children, and the name of + * the sub-group as its label. The first child is the contents + * of the sub-group, and the second is the marker that started + * the subgroup. The marker is a leaf node labeled with its + * contents, and the contents contains a recursive tree. + * + * A group node is a node with three children, and the name of + * the group as its label. The first child is the opening + * delimiter, the second is the group contents, and the third is + * the closing delimiter. The delimiters are leaf nodes labeled + * with their contents, while the group node contains a + * recursive tree. + * + * @throws DelimiterException + * Thrown if something went wrong during sequence + * delimitation. + * + */ + public ITree<T> delimitSequence(final SequenceCharacteristics<T> chars, + @SuppressWarnings("unchecked") final T... seq) throws DelimiterException { + if (initialGroup == null) throw new NullPointerException("Initial group must be specified."); + else if (chars == null) throw new NullPointerException("Sequence characteristics must not be null"); + + /* + * The stack of opened and not yet closed groups. + */ + final Stack<DelimiterGroup<T>.OpenGroup> groupStack = new SimpleStack<>(); + + /* + * Open initial group. + */ + groupStack.push(initialGroup.open(chars.root, null)); + + /* + * Groups that aren't allowed to be opened at the moment. + */ + final Stack<Multiset<T>> forbiddenDelimiters = new SimpleStack<>(); + forbiddenDelimiters.push(HashMultiset.create()); + + /* + * Groups that are allowed to be opened at the moment. + */ + final Stack<Multimap<T, T>> allowedDelimiters = new SimpleStack<>(); + allowedDelimiters.push(HashMultimap.create()); + + /* + * Map of who forbid what for debugging purposes. + */ + final IMap<T, T> whoForbid = new PushdownMap<>(); + + /* + * Process each member of the sequence. + */ + for (int i = 0; i < seq.length; i++) { + final T tok = seq[i]; + + /* + * Check if this token could open a group. + */ + final IPair<T, T[]> possibleOpenPar = groupStack.top().doesOpen(tok); + T possibleOpen = possibleOpenPar.getLeft(); + + if (possibleOpen == null) { + /* + * Handle nested openers. + * + * Local openers take priority over nested ones + * if they overlap. + */ + if (allowedDelimiters.top().containsKey(tok)) { + possibleOpen = allowedDelimiters.top().get(tok).iterator().next(); + } + } + + /* + * If we have an opening delimiter, handle it. + */ + if (possibleOpen != null) { + final DelimiterGroup<T> group = groups.get(possibleOpen); + + /* + * Error on groups that can't open in this + * context. + * + * This means groups that can't occur at the + * top-level of this group, as well as nested + * exclusions from all enclosing groups. + */ + if (isForbidden(groupStack, forbiddenDelimiters, possibleOpen)) { + T forbiddenBy; + + if (whoForbid.containsKey(tok)) { + forbiddenBy = whoForbid.get(tok); + } else { + forbiddenBy = groupStack.top().getName(); + } + + final String ctxList = StringUtils.toEnglishList(groupStack.toArray(), "then"); + + final String fmt = "Group '%s' can't be opened in this context. (forbidden by '%s')\nContext Stack: %s"; + + throw new DelimiterException(String.format(fmt, group, forbiddenBy, ctxList)); + } + + /* + * Add an open group. + */ + final DelimiterGroup<T>.OpenGroup open = group.open(tok, possibleOpenPar.getRight()); + groupStack.push(open); + + /* + * Handle 'forgetful' groups that reset nesting + */ + if (open.isForgetful()) { + allowedDelimiters.push(HashMultimap.create()); + forbiddenDelimiters.push(HashMultiset.create()); + } + + /* + * Add the nested opens from this group. + */ + final Multimap<T, T> currentAllowed = allowedDelimiters.top(); + for (final Entry<T, T> opener : open.getNestingOpeners().entrySet()) { + currentAllowed.put(opener.getKey(), opener.getValue()); + } + + /* + * Add the nested exclusions from this group + */ + final Multiset<T> currentForbidden = forbiddenDelimiters.top(); + for (final T exclusion : open.getNestingExclusions()) { + currentForbidden.add(exclusion); + + whoForbid.put(exclusion, possibleOpen); + } + } else if (!groupStack.empty() && groupStack.top().isClosing(tok)) { + /* + * Close the group. + */ + final DelimiterGroup<T>.OpenGroup closed = groupStack.pop(); + + groupStack.top().addItem(closed.toTree(tok, chars)); + + /* + * Remove nested exclusions from this group. + */ + final Multiset<T> currentForbidden = forbiddenDelimiters.top(); + for (final T excludedGroup : closed.getNestingExclusions()) { + currentForbidden.remove(excludedGroup); + + whoForbid.remove(excludedGroup); + } + + /* + * Remove the nested opens from this group. + */ + final Multimap<T, T> currentAllowed = allowedDelimiters.top(); + for (final Entry<T, T> closer : closed.getNestingOpeners().entrySet()) { + currentAllowed.remove(closer.getKey(), closer.getValue()); + } + + /* + * Handle 'forgetful' groups that reset nesting. + */ + if (closed.isForgetful()) { + allowedDelimiters.drop(); + forbiddenDelimiters.drop(); + } + } else if (!groupStack.empty() && groupStack.top().marksSubgroup(tok)) { + /* + * Mark a subgroup. + */ + groupStack.top().markSubgroup(tok, chars); + } else { + /* + * Add an item to the group. + */ + groupStack.top().addItem(new Tree<>(tok)); + } + } + + /* + * Error if not all groups were closed. + */ + if (groupStack.size() > 1) { + final DelimiterGroup<T>.OpenGroup group = groupStack.top(); + + final StringBuilder msgBuilder = new StringBuilder(); + + final String closingDelims = StringUtils.toEnglishList(group.getNestingExclusions().toArray(), + false); + + final String ctxList = StringUtils.toEnglishList(groupStack.toArray(), "then"); + + msgBuilder.append("Unclosed group '"); + msgBuilder.append(group.getName()); + msgBuilder.append("'. Expected one of "); + msgBuilder.append(closingDelims); + msgBuilder.append(" to close it\nOpen groups: "); + msgBuilder.append(ctxList); + + final String fmt = "Unclosed group '%s'. Expected one of %s to close it.\nOpen groups: %n"; + + throw new DelimiterException(String.format(fmt, group.getName(), closingDelims, ctxList)); + } + + return groupStack.pop().toTree(chars.root, chars); + } + + /* + * Check if a group is forbidden to open in a context. + */ + private boolean isForbidden(final Stack<DelimiterGroup<T>.OpenGroup> groupStack, + final Stack<Multiset<T>> forbiddenDelimiters, final T groupName) { + boolean localForbid; + + /* + * Check if a delimiter is locally forbidden. + */ + if (groupStack.empty()) { + localForbid = false; + } else { + localForbid = groupStack.top().excludes(groupName); + } + + return localForbid || forbiddenDelimiters.top().contains(groupName); + } + + /** + * Add a delimiter group. + * + * @param group + * The delimiter group. + */ + public void addGroup(final DelimiterGroup<T> group) { + if (group == null) throw new NullPointerException("Group must not be null"); + + groups.put(group.groupName, group); + } + + /** + * Creates and adds a delimiter group using the provided settings. + * + * @param openers + * The tokens that open this group + * @param groupName + * The name of the group + * @param closers + * The tokens that close this group + */ + public void addGroup(final T[] openers, final T groupName, @SuppressWarnings("unchecked") final T... closers) { + final DelimiterGroup<T> group = new DelimiterGroup<>(groupName); + + group.addClosing(closers); + + addGroup(group); + + for (final T open : openers) { + group.addOpener(open, groupName); + } + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + + builder.append("SequenceDelimiter ["); + + if (groups != null) { + builder.append("groups="); + builder.append(groups); + builder.append(","); + } + + if (initialGroup != null) { + builder.append("initialGroup="); + builder.append(initialGroup); + } + + builder.append("]"); + + return builder.toString(); + } + + /** + * Set the initial group of this delimiter. + * + * @param initialGroup + * The initial group of this delimiter. + */ + public void setInitialGroup(final DelimiterGroup<T> initialGroup) { + this.initialGroup = initialGroup; + } +} diff --git a/base/src/main/java/bjc/utils/parserutils/delims/StringDelimiter.java b/base/src/main/java/bjc/utils/parserutils/delims/StringDelimiter.java new file mode 100644 index 0000000..e3eeea5 --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/delims/StringDelimiter.java @@ -0,0 +1,31 @@ +package bjc.utils.parserutils.delims; + +import bjc.utils.data.ITree; + +/** + * A sequence delimiter specialized for strings. + * + * @author EVE + * + */ +public class StringDelimiter extends SequenceDelimiter<String> { + + /** + * Override of + * {@link SequenceDelimiter#delimitSequence(SequenceCharacteristics, Object...)} + * for ease of use for strings. + * + * @param seq + * The sequence to delimit. + * + * @return The sequence as a tree. + * + * @throws DelimiterException + * if something went wrong with delimiting the sequence. + * + * @see SequenceDelimiter + */ + public ITree<String> delimitSequence(final String... seq) throws DelimiterException { + return super.delimitSequence(new SequenceCharacteristics<>("root", "contents", "subgroup"), seq); + } +} diff --git a/base/src/main/java/bjc/utils/parserutils/splitter/ChainTokenSplitter.java b/base/src/main/java/bjc/utils/parserutils/splitter/ChainTokenSplitter.java new file mode 100644 index 0000000..4736310 --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/splitter/ChainTokenSplitter.java @@ -0,0 +1,50 @@ +package bjc.utils.parserutils.splitter; + +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.IList; + +/** + * A token splitter that chains several other splitters together. + * + * @author EVE + * + */ +public class ChainTokenSplitter implements TokenSplitter { + private final IList<TokenSplitter> spliters; + + /** + * Create a new chain token splitter. + */ + public ChainTokenSplitter() { + spliters = new FunctionalList<>(); + } + + /** + * Append a series of splitters to the chain. + * + * @param splitters + * The splitters to append to the chain. + */ + public void appendSplitters(final TokenSplitter... splitters) { + spliters.addAll(splitters); + } + + /** + * Prepend a series of splitters to the chain. + * + * @param splitters + * The splitters to append to the chain. + */ + public void prependSplitters(final TokenSplitter... splitters) { + spliters.prependAll(splitters); + } + + @Override + public IList<String> split(final String input) { + final IList<String> initList = new FunctionalList<>(input); + + return spliters.reduceAux(initList, (splitter, strangs) -> { + return strangs.flatMap(splitter::split); + }); + } +}
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/parserutils/splitter/ConfigurableTokenSplitter.java b/base/src/main/java/bjc/utils/parserutils/splitter/ConfigurableTokenSplitter.java new file mode 100644 index 0000000..48ddcb4 --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/splitter/ConfigurableTokenSplitter.java @@ -0,0 +1,122 @@ +package bjc.utils.parserutils.splitter; + +import static bjc.utils.PropertyDB.applyFormat; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.regex.Pattern; + +import bjc.utils.funcdata.IList; + +/** + * Split a string into pieces around a regular expression, and offer an easy way + * to configure the regular expression. + * + * @author EVE + * + */ +public class ConfigurableTokenSplitter extends SimpleTokenSplitter { + private final Set<String> simpleDelimiters; + private final Set<String> multipleDelimiters; + private final Set<String> rRawDelimiters; + + /** + * Create a new token splitter with blank configuration. + * + * @param keepDelims + * Whether or not to keep delimiters. + */ + public ConfigurableTokenSplitter(final boolean keepDelims) { + super(null, keepDelims); + + /* + * Use linked hash-sets to keep items in insertion order. + */ + simpleDelimiters = new LinkedHashSet<>(); + multipleDelimiters = new LinkedHashSet<>(); + rRawDelimiters = new LinkedHashSet<>(); + } + + /** + * Add a set of simple delimiters to this splitter. + * + * Simple delimiters match one occurrence of themselves as literals. + * + * @param simpleDelims + * The simple delimiters to add. + */ + public void addSimpleDelimiters(final String... simpleDelims) { + for (final String simpleDelim : simpleDelims) { + simpleDelimiters.add(simpleDelim); + } + } + + /** + * Add a set of multiple delimiters to this splitter. + * + * Multiple delimiters match one or more occurrences of themselves as + * literals. + * + * @param multiDelims + * The multiple delimiters to add. + */ + public void addMultiDelimiters(final String... multiDelims) { + for (final String multiDelim : multiDelims) { + multipleDelimiters.add(multiDelim); + } + } + + /** + * Add a set of raw delimiters to this splitter. + * + * Raw delimiters match one occurrence of themselves as regular + * expressions. + * + * @param rRawDelims + * The raw delimiters to add. + */ + public void addRawDelimiters(final String... rRawDelims) { + for (final String rRawDelim : rRawDelims) { + rRawDelimiters.add(rRawDelim); + } + } + + /** + * Take the configuration and compile it into a regular expression to + * use when splitting. + */ + public void compile() { + final StringBuilder rPattern = new StringBuilder(); + + for (final String rRawDelimiter : rRawDelimiters) { + rPattern.append(applyFormat("rawDelim", rRawDelimiter)); + } + + for (final String multipleDelimiter : multipleDelimiters) { + rPattern.append(applyFormat("multipleDelim", multipleDelimiter)); + } + + for (final String simpleDelimiter : simpleDelimiters) { + rPattern.append(applyFormat("simpleDelim", simpleDelimiter)); + } + + rPattern.deleteCharAt(rPattern.length() - 1); + + spliter = Pattern.compile(rPattern.toString()); + } + + @Override + public IList<String> split(final String input) { + if (spliter == null) throw new IllegalStateException("Must compile splitter before use"); + + return super.split(input); + } + + @Override + public String toString() { + final String fmt = "ConfigurableTokenSplitter [simpleDelimiters=%s, multipleDelimiters=%s," + + " rRawDelimiters=%s, spliter=%s]"; + + return String.format(fmt, simpleDelimiters, multipleDelimiters, rRawDelimiters, spliter); + } +} diff --git a/base/src/main/java/bjc/utils/parserutils/splitter/ExcludingTokenSplitter.java b/base/src/main/java/bjc/utils/parserutils/splitter/ExcludingTokenSplitter.java new file mode 100644 index 0000000..369e7ae --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/splitter/ExcludingTokenSplitter.java @@ -0,0 +1,71 @@ +package bjc.utils.parserutils.splitter; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; + +import bjc.utils.funcdata.FunctionalList; +import bjc.utils.funcdata.IList; + +/** + * A token splitter that will not split certain tokens. + * + * @author EVE + * + */ +public class ExcludingTokenSplitter implements TokenSplitter { + private final Set<String> literalExclusions; + + private final IList<Predicate<String>> predExclusions; + + private final TokenSplitter spliter; + + /** + * Create a new excluding token splitter. + * + * @param splitter + * The splitter to apply to non-excluded strings. + */ + public ExcludingTokenSplitter(final TokenSplitter splitter) { + spliter = splitter; + + literalExclusions = new HashSet<>(); + + predExclusions = new FunctionalList<>(); + } + + /** + * Exclude literal strings from splitting. + * + * @param exclusions + * The strings to exclude from splitting. + */ + public final void addLiteralExclusions(final String... exclusions) { + for (final String exclusion : exclusions) { + literalExclusions.add(exclusion); + } + } + + /** + * Exclude all of the strings matching any of the predicates from + * splitting. + * + * @param exclusions + * The predicates to use for exclusions. + */ + @SafeVarargs + public final void addPredicateExclusion(final Predicate<String>... exclusions) { + for (final Predicate<String> exclusion : exclusions) { + predExclusions.add(exclusion); + } + } + + @Override + public IList<String> split(final String input) { + if (literalExclusions.contains(input)) + return new FunctionalList<>(input); + else if (predExclusions.anyMatch(pred -> pred.test(input))) + return new FunctionalList<>(input); + else return spliter.split(input); + } +} diff --git a/base/src/main/java/bjc/utils/parserutils/splitter/FilteredTokenSplitter.java b/base/src/main/java/bjc/utils/parserutils/splitter/FilteredTokenSplitter.java new file mode 100644 index 0000000..5d954e0 --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/splitter/FilteredTokenSplitter.java @@ -0,0 +1,37 @@ +package bjc.utils.parserutils.splitter; + +import java.util.function.Predicate; + +import bjc.utils.funcdata.IList; + +/** + * A token splitter that removes tokens that match a predicate from the stream + * of tokens. + * + * @author bjculkin + * + */ +public class FilteredTokenSplitter implements TokenSplitter { + private TokenSplitter source; + + private Predicate<String> filter; + + /** + * Create a new filtered token splitter. + * + * @param source + * The splitter to get tokens from. + * + * @param filter + * The filter to pass tokens through. + */ + public FilteredTokenSplitter(TokenSplitter source, Predicate<String> filter) { + this.source = source; + this.filter = filter; + } + + @Override + public IList<String> split(String input) { + return source.split(input).getMatching(filter); + } +} diff --git a/base/src/main/java/bjc/utils/parserutils/splitter/SimpleTokenSplitter.java b/base/src/main/java/bjc/utils/parserutils/splitter/SimpleTokenSplitter.java new file mode 100644 index 0000000..c357886 --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/splitter/SimpleTokenSplitter.java @@ -0,0 +1,46 @@ +package bjc.utils.parserutils.splitter; + +import java.util.regex.Pattern; + +import bjc.utils.funcdata.IList; +import bjc.utils.functypes.ID; +import bjc.utils.ioutils.RegexStringEditor; + +/** + * Splits a string into pieces around a regular expression. + * + * @author EVE + * + */ +public class SimpleTokenSplitter implements TokenSplitter { + protected Pattern spliter; + + private final boolean keepDelim; + + /** + * Create a new simple token splitter. + * + * @param splitter + * The pattern to split around. + * + * @param keepDelims + * Whether or not delimiters should be kept. + */ + public SimpleTokenSplitter(final Pattern splitter, final boolean keepDelims) { + spliter = splitter; + + keepDelim = keepDelims; + } + + @Override + public IList<String> split(final String input) { + if (keepDelim) + return RegexStringEditor.mapOccurances(input, spliter, ID.id(), ID.id()); + else return RegexStringEditor.mapOccurances(input, spliter, ID.id(), strang -> ""); + } + + @Override + public String toString() { + return String.format("SimpleTokenSplitter [spliter=%s, keepDelim=%s]", spliter, keepDelim); + } +} diff --git a/base/src/main/java/bjc/utils/parserutils/splitter/TokenSplitter.java b/base/src/main/java/bjc/utils/parserutils/splitter/TokenSplitter.java new file mode 100644 index 0000000..ddb28a7 --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/splitter/TokenSplitter.java @@ -0,0 +1,21 @@ +package bjc.utils.parserutils.splitter; + +import bjc.utils.funcdata.IList; + +/** + * Split a string into a list of pieces. + * + * @author EVE + * + */ +public interface TokenSplitter { + /** + * Split a string into a list of pieces. + * + * @param input + * The string to split. + * + * @return The pieces of the string. + */ + public IList<String> split(String input); +} diff --git a/base/src/main/java/bjc/utils/parserutils/splitter/TransformTokenSplitter.java b/base/src/main/java/bjc/utils/parserutils/splitter/TransformTokenSplitter.java new file mode 100644 index 0000000..80490f5 --- /dev/null +++ b/base/src/main/java/bjc/utils/parserutils/splitter/TransformTokenSplitter.java @@ -0,0 +1,38 @@ +package bjc.utils.parserutils.splitter; + +import java.util.function.UnaryOperator; + +import bjc.utils.funcdata.IList; + +/** + * A token splitter that performs a transform on the tokens from another + * splitter. + * + * @author bjculkin + * + */ +public class TransformTokenSplitter implements TokenSplitter { + private TokenSplitter source; + + private UnaryOperator<String> transform; + + /** + * Create a new transforming splitter. + * + * @param source + * The splitter to use as a source. + * + * @param transform + * The transform to apply to tokens. + */ + public TransformTokenSplitter(TokenSplitter source, UnaryOperator<String> transform) { + this.source = source; + this.transform = transform; + } + + @Override + public IList<String> split(String input) { + return source.split(input).map(transform); + } + +} diff --git a/base/src/test/java/bjc/utils/test/parserutils/TokenUtilsTest.java b/base/src/test/java/bjc/utils/test/parserutils/TokenUtilsTest.java new file mode 100644 index 0000000..6fba1b2 --- /dev/null +++ b/base/src/test/java/bjc/utils/test/parserutils/TokenUtilsTest.java @@ -0,0 +1,152 @@ +package bjc.utils.test.parserutils; + +import static bjc.utils.parserutils.TokenUtils.descapeString; +import static bjc.utils.parserutils.TokenUtils.removeDQuotedStrings; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import java.util.List; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +/* + * Tests for TokenUtils + */ +public class TokenUtilsTest { + @Rule + public ExpectedException exp = ExpectedException.none(); + + /* + * Test removeDQuoted + */ + + /* + * Check handling of mismatched strings with no matching strings. + */ + @Test + public void testRemoveDQuoted_MismatchedStringNoMatch() throws IllegalArgumentException { + exp.expect(IllegalArgumentException.class); + exp.expectMessage(containsString("Opening quote was at position 0")); + + removeDQuotedStrings("\"hello"); + } + + /* + * Check handling of mismatched strings with a matching string. + */ + @Test + public void testRemoveDQuoted_MismatchedStringMatch() throws IllegalArgumentException { + exp.expect(IllegalArgumentException.class); + exp.expectMessage(containsString("Opening quote was at position 7")); + + removeDQuotedStrings("\"hello\"\""); + } + + /* + * Check handling of strings with a single embedded string. + */ + @Test + public void testRemoveDQuoted_SingleString() { + final List<String> onSingleMatchString = removeDQuotedStrings("hello\"there\""); + + assertThat(onSingleMatchString, hasItems("hello", "\"there\"")); + } + + /* + * Check handling of strings with multiple quoted strings in a row. + */ + @Test + public void testRemoveDQuoted_MultipleSerialString() { + final List<String> onMultipleSerialMatchString = removeDQuotedStrings("\"hello\"\"there\""); + + assertThat(onMultipleSerialMatchString, hasItems("\"hello\"", "\"there\"")); + } + + /* + * Check handling of strings with multiple interleaved strings. + */ + @Test + public void testRemoveDQuoted_MultipleInterleavedString() { + final List<String> onMultipleInterleaveMatchString = removeDQuotedStrings("one\"two\"three\"four\""); + + assertThat(onMultipleInterleaveMatchString, hasItems("one", "\"two\"", "three", "\"four\"")); + } + + /* + * Check handling of strings without embedded strings. + */ + @Test + public void testRemoveDQuote_NoString() { + final List<String> onNonmatchingString = removeDQuotedStrings("hello"); + + assertThat(onNonmatchingString, hasItems("hello")); + } + + /* + * Check handling of empty strings. + */ + @Test + public void testRemoveDQuote_EmptyString() { + final List<String> onEmptyString = removeDQuotedStrings(""); + + assertThat(onEmptyString, hasItems("")); + } + + /* + * Test descapeString + */ + /* + * Check handling of empty strings. + */ + @Test + public void testDescapeString_EmptyString() { + final String onEmptyString = descapeString(""); + + assertThat(onEmptyString, is("")); + } + + /* + * Check handling of strings without escapes + */ + @Test + public void testDescapeString_NonescapeString() { + final String onNonescapeString = descapeString("hello there"); + + assertThat(onNonescapeString, is("hello there")); + } + + /* + * Check handling of strings with single escapes. + */ + @Test + public void testDescapeString_SingleEscapeString() { + final String onSingleEscapeString = descapeString("hello\\tthere"); + + assertThat(onSingleEscapeString, is("hello\tthere")); + } + + /* + * Check handling of strings with multiple escapes. + */ + @Test + public void testDescapeString_MultipleEscapeString() { + final String onMultipleEscapeString = descapeString("hello\\tthere\\tworld"); + + assertThat(onMultipleEscapeString, is("hello\tthere\tworld")); + } + + /* + * Check handling of strings with invalid single escapes. + */ + @Test + public void testDescapeString_InvalidSingleEscapeString() throws IllegalArgumentException { + exp.expect(IllegalArgumentException.class); + exp.expectMessage(containsString("at position 0")); + + descapeString("\\x"); + } +}
\ No newline at end of file |
