Repository: PlasmaPy/PlasmaPy

Issue:
Unexpected behaviour in 'Particle' when the argument has a trailing space
<!--

Thanks for opening an issue! To help us react appropriately to your report,
please first look through https://github.com/PlasmaPy/PlasmaPy/issues and check
whether no other issue already describes your problem.

For more general "how do I do X?" type questions, please speak to us in real
time on https://app.element.io/#/room/#plasmapy:openastronomy.org or ask on
Github Discussions: https://github.com/plasmapy/plasmapy/discussions . We'll be
more than happy to help! :)

-->

## Reporting a bug

<!--

Before submitting a bug report please ensure that you can check off these boxes:

-->

- [x] I am using the latest released version of PlasmaPy (most recent is visible in
 the release notes (http://docs.plasmapy.org/en/stable/about/release_notes.html).
- [x] I have included below a minimal working reproducer (if you are unsure how
 to write one see http://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports).

I have noticed a strange behavior when a 'Particle' is created from an atom symbol with a trailing space, the result is a negative ion. For example:

```python
>>>import plasmapy
>>>plasmapy.particles.Particle("He ")
Particle("He 1-")
```
I think the issue is in _parsing.py::extract_charge and could easily be fixed.
I suppose this case should raise an exception as it is the case with `Particle("e ")`.




Discussion:
It seems that an empty string is a valid roman numeral. the issue is caused in _parsing.py line 186 (on main). Possibles fix are either changing the regex that validates roman numerals, or catching the issue earlier by detecting trailing spaces in the argument validation function. 
Thank you for bring this up!  I agree this looks like it's an issue in `_parsing.extract_charge()`.  Specifically I think it's a "bigger" issue related to how that if clause your referenced is splitting the string based on `" "`.  For example, you also get errors doing `"Ar +1"` (2 blank spaces between `Ar` and `+1`) while you'd expect it to default back to `"Ar +1"` (1 blank space).

Resolving patch:
diff --git a/plasmapy/particles/_parsing.py b/plasmapy/particles/_parsing.py
--- a/plasmapy/particles/_parsing.py
+++ b/plasmapy/particles/_parsing.py
@@ -166,8 +166,23 @@ def extract_charge(arg: str):
         f"Invalid charge information in the particle string '{arg}'."
     )
 
-    if arg.count(" ") == 1:  # Cases like 'H 1-' and 'Fe-56 1+'
-        isotope_info, charge_info = arg.split(" ")
+    match = re.fullmatch(
+        r"\s*(?P<isotope>\w+(-[0-9]+)*([+-]*)*)"
+        r"(\s*(?P<charge>[+-]?[IVXLCDM0-9]+[+-]?)*)*\s*",
+        arg,
+    )
+
+    if match is None:
+        raise InvalidParticleError(invalid_charge_errmsg) from None
+
+    isotope_info = match.groupdict()["isotope"]
+    charge_info = match.groupdict()["charge"]
+    Z_from_arg = None
+
+    if charge_info is not None and isotope_info.endswith(("-", "+")):
+        # charge info is defined on both the charge_info and isotope_into strings
+        raise InvalidParticleError(invalid_charge_errmsg) from None
+    elif charge_info is not None:  # Cases like 'H 1-' and 'Fe-56 1+'
 
         sign_indicator_only_on_one_end = charge_info.endswith(
             ("-", "+")
@@ -194,19 +209,20 @@ def extract_charge(arg: str):
         except ValueError:
             raise InvalidParticleError(invalid_charge_errmsg) from None
 
-    elif arg.endswith(("-", "+")):  # Cases like 'H-' and 'Pb-209+++'
-        char = arg[-1]
-        match = re.match(f"[{char}]*", arg[::-1])
-        Z_from_arg = match.span()[1]
-        isotope_info = arg[: len(arg) - match.span()[1]]
+    elif isotope_info.endswith(("-", "+")):  # Cases like 'H-' and 'Pb-209+++'
+        match = re.fullmatch(
+            r"\s*(?P<isotope>\w+(-[0-9]+)?)(?P<charge>[-+]+)\s*",
+            isotope_info,
+        )
+        isotope_info = match.groupdict()["isotope"]
+        charge_info = match.groupdict()["charge"]
+
+        if len(set(charge_info)) != 1:
+            raise InvalidParticleError(invalid_charge_errmsg) from None
 
-        if char == "-":
+        Z_from_arg = len(charge_info)
+        if charge_info[0] == "-":
             Z_from_arg = -Z_from_arg
-        if isotope_info.endswith(("-", "+")):
-            raise InvalidParticleError(invalid_charge_errmsg) from None
-    else:
-        isotope_info = arg
-        Z_from_arg = None
 
     return isotope_info, Z_from_arg
 


Test patch: