简体   繁体   中英

How to parse dice notation with a Java regular expression?

Standard RPG dice notation is something like this: "AdB[x/]C[+-]D", where A, B, C, D are natural numbers: A the number of dice to roll, B the number of sides on each die, C a following multiplier or divisor, and D an addition or subtraction.

Some examples:

  • d6: Roll a single 6-sided die.
  • 3d6: Roll 3 dice of 6-sides each and find the sum.
  • 4d6+1: Roll 4 dice of 6-sides each, and add 1 to the sum.
  • 3d6x10: Roll 3 dice of 6-sides each, then multiply the sum by 10.
  • d6/2: Roll a 6-sided die and then divide by 2 (rounding up).

I'd like a Java method that gets fed a string in this format and parses out the correct integer values of A, B, C, and D. (Say: If C is meant as a divisor, then store a negative int; likewise if D is meant as a subtraction do the same.) I've actually implemented this already with some loops looking at individual character values in the String, but it's ugly as sin.

I have the intuition that this would be more concisely solved by using a regular expression, but frankly I'm completely ignorant in that topic. Any suggestions on how that would be done? And if there's a more elegant abstraction to the problem (like say, an arbitrary number of separate x/+- modifiers; or needing to turn the x into *) I'd be open to that, too.

This question gives a closely related regular expression, but doesn't illustrate how to extract the parameters from it: Java Regex repetition (Dice notation parsing)

This named-group example yields some promising results from your sample input.

Note : Requires Java 7 for named regex groups.

String pattern = "(?<A>\\d*)d((?<B>\\d+)(?<math>(?<mult>[x\\/](?<C>\\d+))?(?<add>[+-](?<D>\\d+))?)?)?";
Pattern p = Pattern.compile(pattern);

String[] tests = new String[] {
    "d6", "3d6", "4d6+1", "3d6x10", "d6/2", "3d4/2-7", "12d4-", "d-8", "4dx"
};

for (String test : tests) {
    System.out.printf("Testing \"%s\"\n", test);
    Matcher m = p.matcher(test);

    if (m.matches()) {
        String groupA = m.group("A");
        if (groupA == null) {
            groupA = "1"; // default one roll
        }

        String groupB = m.group("B");
        if (groupB == null) {
            groupB = "6"; // default six-sided die
        }

        String groupC = m.group("C");
        if (groupC == null) {
            groupC = "1"; // default multiply or divide by 1
        }

        String groupD = m.group("D");
        if (groupD == null) {
            groupD = "0"; // default add or subtract 0
        }

        int a = Integer.parseInt(groupA);
        int b = Integer.parseInt(groupB);
        int c = Integer.parseInt(groupC);
        int d = Integer.parseInt(groupD);

        String groupMath = m.group("math");
        if (groupMath != null && groupMath.isEmpty()) {
            groupMath = null;
        }
        String groupAdd = m.group("add");
        String groupMult = m.group("mult");

        System.out.printf("A: %d\n", a);
        System.out.printf("B: %d\n", b);
        System.out.printf("C: %d\n", c);
        System.out.printf("D: %d\n", d);
        System.out.println("------");
        System.out.printf("math: %s\n", groupMath);
        System.out.printf("mult: %s\n", groupMult);
        System.out.printf("add: %s\n", groupAdd);
    } else {
        System.out.println("No Match!");
    }

    System.out.println();
}

Output

Testing "d6"
A: 1
B: 6
C: 1
D: 0
------
math: null
mult: null
add: null

Testing "3d6"
A: 3
B: 6
C: 1
D: 0
------
math: null
mult: null
add: null

Testing "4d6+1"
A: 4
B: 6
C: 1
D: 1
------
math: +1
mult: null
add: +1

Testing "3d6x10"
A: 3
B: 6
C: 10
D: 0
------
math: x10
mult: x10
add: null

Testing "d6/2"
A: 1
B: 6
C: 2
D: 0
------
math: /2
mult: /2
add: null

Testing "3d4/2-7"
A: 3
B: 4
C: 2
D: 7
------
math: /2-7
mult: /2
add: -7

Testing "12d4-"
No Match!

Testing "d-8"
No Match!

Testing "4dx"
No Match!

This isn't the smartest or most elegant way, but it's short, readable and robust. Since I'm not sure how you intend to use it I didn't add anything fancy for outputting or making this into an API/library:

public class DieRegex {

    public static void main(String[] args) {

        int amount, die, mult = 1, add = 0;

        Pattern p = Pattern.compile("([1-9]\\d*)?d([1-9]\\d*)([/x][1-9]\\d*)?([+-]\\d+)?");
        Matcher m = p.matcher("d20");
        if (m.matches()) {
            amount = (m.group(1) != null) ? Integer.parseInt(m.group(1)) : 1;
            die = Integer.parseInt(m.group(2));
            if (m.group(3) != null) {
                boolean positive = m.group(3).startsWith("x");
                int val = Integer.parseInt(m.group(3).substring(1));
                mult = positive ? val : -val;
            }
            if (m.group(4) != null) {
                boolean positive = m.group(4).startsWith("+");
                int val = Integer.parseInt(m.group(4).substring(1));
                add = positive ? val : -val;
            }
        }
        else
            System.out.println("No match"); // Do whatever you need
    }
}

Note : According to the comments about checking the input:

  • amount (A) die (B) and mult (C) must be greater than 0. However, positive numbers with leading zeros are not allowed ( 02d20 is not valid, 2d20 is the correct form). This can be fixed with a lookahead but will make the regex more complicated.
  • add (D) can be any number (even with leading zeros).
  • mult (C) add (D) are 1 and 0 respectively if they don't exist in the string. It's just a default value that indicates "no value".
  • The format that is checked for:
    • A: [optional] non-negative number (default 1)
    • d
    • B: [required] non-negative number
    • C: [optional] / or x followed by a non-negative number (default 1)
    • D: [optional] + or - followed by a non-negative number (default 0)

I didn't extensively test it, but according to your explanation the following code works. May need some minor tweaking of the regex if you notice something's wrong. Biggest difference from this version to the rest of others is that it's modularized within a Class called RPGDice that you could instantiate with RPGDice.parse(expr) and get an RPGDice instance that contains the different attributes (rolls, faces, multiplier, additive).

public class RPGDice {

    private static final Pattern DICE_PATTERN = Pattern.compile("(?<A>\\d*)d(?<B>\\d+)(?>(?<MULT>[x/])(?<C>\\d+))?(?>(?<ADD>[+-])(?<D>\\d+))?");

    private int rolls = 0;
    private int faces = 0;
    private int multiplier = 0;
    private int additive = 0;

    public RPGDice(int rolls, int faces, int multiplier, int additive) {
        this.rolls = rolls;
        this.faces = faces;
        this.multiplier = multiplier;
        this.additive = additive;
    }

    public int getRolls() {
        return rolls;
    }

    public int getFaces() {
        return faces;
    }

    public int getMultiplier() {
        return multiplier;
    }

    public int getAdditive() {
        return additive;
    }

    @Override
    public String toString() {
        return String.format("{\"rolls\": %s, \"faces\": %s, \"multiplier\": %s, \"additive\": %s}", rolls, faces, multiplier, additive);
    }

    private static boolean isEmpty(String str) {
        return str == null || str.trim().isEmpty();
    }

    private static Integer getInt(Matcher matcher, String group, int defaultValue) {
        String groupValue = matcher.group(group);
        return isEmpty(groupValue) ? defaultValue : Integer.valueOf(groupValue);
    }

    private static Integer getSign(Matcher matcher, String group, String positiveValue) {
        String groupValue = matcher.group(group);
        return isEmpty(groupValue) || groupValue.equals(positiveValue) ? 1 : -1;
    }

    public static RPGDice parse(String str) {
        Matcher matcher = DICE_PATTERN.matcher(str);
        if(matcher.matches()) {
            int rolls = getInt(matcher, "A", 1);
            int faces = getInt(matcher, "B", -1);
            int multiplier = getInt(matcher, "C", 1);
            int additive = getInt(matcher, "D", 0);
            int multiplierSign = getSign(matcher, "MULT", "x");
            int additiveSign = getSign(matcher, "ADD", "+");
            return new RPGDice(rolls, faces, multiplier * multiplierSign, additive * additiveSign);
        }
        return null;
        // OR
        // throw new IllegalArgumentException("Invalid Expression");
    }

    public static void main(String[] args) {
        System.out.println(RPGDice.parse("d6"));
        System.out.println(RPGDice.parse("d6x"));
        System.out.println(RPGDice.parse("33d6x10"));
        System.out.println(RPGDice.parse("336x10"));
        System.out.println(RPGDice.parse("d6/"));
        System.out.println(RPGDice.parse("d6/5"));
        System.out.println(RPGDice.parse("d6/5+2"));
        System.out.println(RPGDice.parse("2d6/5-32"));
        System.out.println(RPGDice.parse("2d6/5+-32"));
    }
}

Output:

{"rolls": 1, "faces": 6, "multiplier": 1, "additive": 0}
null
{"rolls": 33, "faces": 6, "multiplier": 10, "additive": 0}
null
null
{"rolls": 1, "faces": 6, "multiplier": -5, "additive": 0}
{"rolls": 1, "faces": 6, "multiplier": -5, "additive": 2}
{"rolls": 2, "faces": 6, "multiplier": -5, "additive": -32}
null

Simple Answer

Consider reading the manual. RTxM1 and RTxM2

TLDR

the output of the code example below shows what you need to know to parse the dice notation using a regex.

public class RegEx
{
    private static final int EXPECTED_GROUP_COUNT = 7;

    private static final Pattern pattern = Pattern.compile("(\\d+)?[Dd](\\d+)([Xx/](\\d+))?(([+-])(\\d+))?");

    private static void matchit(final String value)
    {
        final Matcher matcher = pattern.matcher(value);

        if (matcher.matches())
        {
            final int groupCount;
            final MatchResult matchResult = matcher.toMatchResult();

            groupCount = matchResult.groupCount();

            System.out.println("kapow: " + value + ", groups: " + groupCount);

            if (groupCount == EXPECTED_GROUP_COUNT)
            {
                for (int index = 0; index <= groupCount; ++index)
                {
                    final String currentGroup = matchResult.group(index);
                    System.out.println("\tgroup[" + index + "]: " + currentGroup);
                }
            }
            else
            {
                System.out.println("match error; wrong group count");
            }
        }
        else
        {
            System.out.println("Format not recognized: " + value);
        }
    }

    public static void main(
        String[] args)
    {
        final String[] thingArray =
        {
            "3d6",
            "d7",
            "4D6+4",
            "3d6x10",
            "d6/2"
        };

        for (final String thing : thingArray)
        {
            matchit(thing);
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM