diff --git a/geodesy/resources/nkg.md b/geodesy/resources/nkg.md new file mode 100644 index 00000000..2d7a6594 --- /dev/null +++ b/geodesy/resources/nkg.md @@ -0,0 +1,153 @@ +# NKG Register + +## Geodesy implementations + +This section contains the Rust Geodesy (RG) implementations of the NKG transformations from ITRF2014 to the national realizations of ETRS89 + +### Sweden + +```geodesy:itrf2014-sweref99 +| adapt from=neuf_deg +| cart ellps=GRS80 +| helmert +: drx = 0.000085 dry = 0.000531 drz = -0.00077 ds = 0 +: t_epoch=1989 convention=position_vector +| deformation +: inv t_epoch=2000.0 +: grids=eur_nkg_nkgrf17vel.deformation +| helmert +: x = 0.03054 rx = 0.00141958 +: y = 0.04606 ry = 0.00015132 +: z =-0.07944 rz = 0.00150337 +: s = 0.003002 +: convention=position_vector +| deformation dt=0.5 grids=eur_nkg_nkgrf17vel.deformation +| cart inv ellps=GRS80 +| adapt to=neuf_deg +``` + +### Denmark + +The 'dt' of the last deformation step in the DK transformation seems odd: +It does not seem to agree with the realization epoch of 1994.704 (i.e. 1994-09-15). +This is due to a minor adjustment of the DK realization at epoch 2015, in effect +solid-body lifting it from the old passive markers onto the active CORS network. + +The RG implementation here fits extremely well with the PROJ implementation (to +at least 12 decimal places) at the test point (55N, 12E) - potentially because the +deformation is much smaller in Copenhagen than in Stockholm. + +```geodesy:itrf2014-etrs89dk +| adapt from=neuf_deg +| cart ellps=GRS80 +| helmert +: drx = 0.000085 dry = 0.000531 drz = -0.00077 +: t_epoch = 1989 convention = position_vector +| deformation inv + t_epoch=2000.0 grids=eur_nkg_nkgrf17vel.deformation +| helmert +: x = 0.66818 rx = 0.00312883 +: y = 0.04453 ry =-0.02373423 +: z =-0.45049 rz = 0.00442969 +: s =-0.003136 convention=position_vector +| deformation inv +: dt=15.829 grids=eur_nkg_nkgrf17vel.deformation +| cart inv ellps=GRS80 +| adapt to=neuf_deg +``` + +```geodesy:test +| adapt from=neuf_deg +| cart ellps=GRS80 +| helmert +: drx = 0.000085 dry = 0.000531 drz = -0.00077 +: t_epoch = 1989 convention = position_vector +| helmert +: x = 0.66818 rx = 0.00312883 +: y = 0.04453 ry =-0.02373423 +: z =-0.45049 rz = 0.00442969 +: s =-0.003136 convention=position_vector +| cart inv ellps=GRS80 +| adapt to=neuf_deg +``` + +## PROJ implementations + +### PROJ Sweden + +Material extracted from PROJ, using the projinfo incantation +below. Output slightly edited for readability + +```console +$ projinfo -o proj -s itrf2014 -t sweref99 + +Operation No. 1: +Conversion from ITRF2014 (geog2D) to ITRF2014 (geocentric) + +ITRF2014 to ETRF2014 (1) + +Inverse of NKG_ETRF14 to ETRF2014 + +NKG_ETRF14 to ETRF97@2000.0 + +ETRF97@2000.0 to ETRF97@1999.5 + +Conversion from SWEREF99 (geocentric) to SWEREF99 (geog2D) +0.02 m, Sweden - onshore and offshore. + ++proj=pipeline + +step +proj=axisswap +order=2,1 + +step +proj=unitconvert +xy_in=deg +xy_out=rad + +step +proj=cart +ellps=GRS80 + +step +proj=helmert +x=0 +y=0 +z=0 +rx=0 +ry=0 +rz=0 +s=0 +dx=0 +dy=0 +dz=0 + +drx=8.5e-05 +dry=0.000531 +drz=-0.00077 +ds=0 +t_epoch=1989 + +convention=position_vector + +step +inv +proj=deformation +t_epoch=2000.0 +grids=eur_nkg_nkgrf17vel.tif + +step +proj=helmert +x=0.03054 +y=0.04606 +z=-0.07944 +rx=0.00141958 + +ry=0.00015132 +rz=0.00150337 +s=0.003002 +convention=position_vector + +step +proj=deformation +dt=-0.5 +grids=eur_nkg_nkgrf17vel.tif + +step +inv +proj=cart +ellps=GRS80 + +step +proj=unitconvert +xy_in=rad +xy_out=deg + +step +proj=axisswap +order=2,1 +``` + +Note that the direction of the last deformation has been swapped by swapping the +sign of 'dt', as this seems to fit best with PROJ at the test point (59N, 18E). +Still some tiny differences (1/100 mm), though - and some unclear things about the +interpretation of Fwd and Inv wrt. the deformation model implementation + +### PROJ Denmark + +Material extracted from PROJ, using the projinfo incantation +below. Output slightly edited for readability + +```console +$ projinfo -o proj -s itrf2014 -t etrs89 --area Denmark + +Conversion from ITRF2014 (geog2D) to ITRF2014 (geocentric) + +ITRF2014 to ETRF2014 (1) + +Inverse of NKG_ETRF14 to ETRF2014 + +NKG_ETRF14 to ETRF92@2000.0 + +ETRF92@2000.0 to ETRF92@1994.704 + +Conversion from ETRS89 (geocentric) to ETRS89 (geog2D) +0.02 m, Denmark - onshore and offshore + ++proj=pipeline + +step +proj=axisswap +order=2,1 + +step +proj=unitconvert +xy_in=deg +xy_out=rad + +step +proj=cart +ellps=GRS80 + +step +proj=helmert +x=0 +y=0 +z=0 +rx=0 +ry=0 +rz=0 +s=0 +dx=0 +dy=0 +dz=0 + +drx=8.5e-05 +dry=0.000531 +drz=-0.00077 +ds=0 +t_epoch=1989 + +convention=position_vector + +step +inv +proj=deformation +t_epoch=2000.0 +grids=eur_nkg_nkgrf17vel.tif + +step +proj=helmert +x=0.66818 +y=0.04453 +z=-0.45049 +rx=0.00312883 + +ry=-0.02373423 +rz=0.00442969 +s=-0.003136 +convention=position_vector + +step +proj=deformation +dt=15.829 +grids=eur_nkg_nkgrf17vel.tif + +step +inv +proj=cart +ellps=GRS80 + +step +proj=unitconvert +xy_in=rad +xy_out=deg + +step +proj=axisswap +order=2,1 +``` + +The 'dt' of the last deformation step in the DK transformation seems odd: +It does not seem to agree with the realization epoch of 1994.704 (i.e. 1994-09-15). +This is due to a minor adjustment of the DK realization at epoch 2015, in effect +solid-body lifting it from the old passive markers onto the active CORS network. + +The RG implementation here fits extremely well with the PROJ implementation (to +at least 12 decimal places) at the test point (55N, 12E) - potentially because the +deformation is much smaller in Copenhagen than in Stockholm. diff --git a/geodesy/resources/nkg.register b/geodesy/resources/nkg.register deleted file mode 100644 index a1947133..00000000 --- a/geodesy/resources/nkg.register +++ /dev/null @@ -1,113 +0,0 @@ - -# Material extracted from PROJ, using the projinfo incantation -# below. Output slightly edited for readability -# -# $ projinfo -o proj -s itrf2014 -t sweref99 -# -# Operation No. 1: -# Conversion from ITRF2014 (geog2D) to ITRF2014 (geocentric) + -# ITRF2014 to ETRF2014 (1) + -# Inverse of NKG_ETRF14 to ETRF2014 + -# NKG_ETRF14 to ETRF97@2000.0 + -# ETRF97@2000.0 to ETRF97@1999.5 + -# Conversion from SWEREF99 (geocentric) to SWEREF99 (geog2D) -# -# 0.02 m, Sweden - onshore and offshore. -# -# -# +proj=pipeline -# +step +proj=axisswap +order=2,1 -# +step +proj=unitconvert +xy_in=deg +xy_out=rad -# +step +proj=cart +ellps=GRS80 -# +step +proj=helmert +x=0 +y=0 +z=0 +rx=0 +ry=0 +rz=0 +s=0 +dx=0 +dy=0 +dz=0 -# +drx=8.5e-05 +dry=0.000531 +drz=-0.00077 +ds=0 +t_epoch=1989 -# +convention=position_vector -# +step +inv +proj=deformation +t_epoch=2000.0 +grids=eur_nkg_nkgrf17vel.tif -# +step +proj=helmert +x=0.03054 +y=0.04606 +z=-0.07944 +rx=0.00141958 -# +ry=0.00015132 +rz=0.00150337 +s=0.003002 +convention=position_vector -# +step +proj=deformation +dt=-0.5 +grids=eur_nkg_nkgrf17vel.tif -# +step +inv +proj=cart +ellps=GRS80 -# +step +proj=unitconvert +xy_in=rad +xy_out=deg -# +step +proj=axisswap +order=2,1 - -# Note that the direction of the last deformation has been swapped by swapping the -# sign of 'dt', as this seems to fit best with PROJ at the test point (59N, 18E). -# Still some tiny differences (1/100 mm), though - and some unclear things about the -# interpretation of Fwd and Inv wrt. the deformation model implementation - -| adapt from=neuf_deg -| cart ellps=GRS80 -| helmert - drx = 0.000085 dry = 0.000531 drz = -0.00077 ds = 0 - t_epoch=1989 convention=position_vector -| deformation inv t_epoch=2000.0 grids=eur_nkg_nkgrf17vel.deformation -| helmert - x = 0.03054 rx = 0.00141958 - y = 0.04606 ry = 0.00015132 - z =-0.07944 rz = 0.00150337 - s = 0.003002 - convention=position_vector -| deformation dt=0.5 grids=eur_nkg_nkgrf17vel.deformation -| cart inv ellps=GRS80 -| adapt to=neuf_deg - - - -# Material extracted from PROJ, using the projinfo incantation -# below. Output slightly edited for readability -# -# $ projinfo -o proj -s itrf2014 -t etrs89 --area Denmark -# -# Conversion from ITRF2014 (geog2D) to ITRF2014 (geocentric) + -# ITRF2014 to ETRF2014 (1) + -# Inverse of NKG_ETRF14 to ETRF2014 + -# NKG_ETRF14 to ETRF92@2000.0 + -# ETRF92@2000.0 to ETRF92@1994.704 + -# Conversion from ETRS89 (geocentric) to ETRS89 (geog2D) -# 0.02 m, Denmark - onshore and offshore -# -# +proj=pipeline -# +step +proj=axisswap +order=2,1 -# +step +proj=unitconvert +xy_in=deg +xy_out=rad -# +step +proj=cart +ellps=GRS80 -# +step +proj=helmert +x=0 +y=0 +z=0 +rx=0 +ry=0 +rz=0 +s=0 +dx=0 +dy=0 +dz=0 -# +drx=8.5e-05 +dry=0.000531 +drz=-0.00077 +ds=0 +t_epoch=1989 -# +convention=position_vector -# +step +inv +proj=deformation +t_epoch=2000.0 +grids=eur_nkg_nkgrf17vel.tif -# +step +proj=helmert +x=0.66818 +y=0.04453 +z=-0.45049 +rx=0.00312883 -# +ry=-0.02373423 +rz=0.00442969 +s=-0.003136 +convention=position_vector -# +step +proj=deformation +dt=15.829 +grids=eur_nkg_nkgrf17vel.tif -# +step +inv +proj=cart +ellps=GRS80 -# +step +proj=unitconvert +xy_in=rad +xy_out=deg -# +step +proj=axisswap +order=2,1 -# -# The 'dt' of the last deformation step in the DK transformation seems odd: -# It does not seem to agree with the realization epoch of 1994.704 (i.e. 1994-09-15). -# This is due to a minor adjustment of the DK realization at epoch 2015, in effect -# solid-body lifting it from the old passive markers onto the active CORS network. -# -# The RG implementation here fits extremely well with the PROJ implementation (to -# at least 12 decimal places) at the test point (55N, 12E) - potentially because the -# deformation is much smaller in Copenhagen than in Stockholm. - - -| adapt - from=neuf_deg -| cart - ellps=GRS80 -| helmert - drx = 0.000085 dry = 0.000531 drz = -0.00077 - t_epoch = 1989 convention = position_vector -| deformation inv - t_epoch=2000.0 grids=eur_nkg_nkgrf17vel.deformation -| helmert - x = 0.66818 rx = 0.00312883 - y = 0.04453 ry =-0.02373423 - z =-0.45049 rz = 0.00442969 - s =-0.003136 convention=position_vector -| deformation inv - dt=15.829 grids=eur_nkg_nkgrf17vel.deformation -| cart inv - ellps=GRS80 -| adapt - to=neuf_deg diff --git a/geodesy/resources/stupid.md b/geodesy/resources/stupid.md new file mode 100644 index 00000000..5741666a --- /dev/null +++ b/geodesy/resources/stupid.md @@ -0,0 +1,69 @@ +# Stupid ways of doing things - but useful for testing + +## Stupid way of adding three + +```geodesy:way_three +addone | addone inv | addone | addone | addone +``` + +## Yep! - adding two, too + +```geodesy:way_too +addone | addone inv | addone | addone +``` + +## Another name for a stupid way of adding two + +```geodesy:way_two + +addone | addone inv | addone | addone +``` + +## Make Helmert do the hard work + +```geodesy:addone +helmert x=1 +``` + +## Add one unless a different value for x is supplied + +```geodesy:add_x +helmert x=(1) +``` + +## Add whichever value of 'something' is supplied + +```geodesy:add_something +helmert x=$something +``` + +## And use the ones above in stupid ways + +```geodesy:addthree_one_by_one +stupid:addone | stupid:addone | stupid:add_x x=-1 | stupid:add_x x=2 +``` + +```geodesy:addthree +stupid:addone | stupid:add_something something=2 +``` + +```geodesy:bad +stupid:addone | stupid:add_something +``` + +## Tests + +```console +$ echo 55 12 | cargo r -- stupid:bad +> Error: Syntax error: 'Incomplete definition for 'x' ('something' not found)' +``` + +```console +$ echo 55 12 | cargo r -- stupid:addthree +> 58.0000000000 12.0000000000 +``` + +```console +$ echo 55 12 | cargo r -- stupid:addthree_one_by_one +> 58.0000000000 12.0000000000 +``` diff --git a/geodesy/resources/stupid.register b/geodesy/resources/stupid.register deleted file mode 100644 index dbac0234..00000000 --- a/geodesy/resources/stupid.register +++ /dev/null @@ -1,13 +0,0 @@ -# Stupid way of adding three - -addone | addone inv | addone | addone | addone - - -# Stupid way of adding two - -addone | addone inv | addone | addone - - -# Another name for a stupid way of adding two - -addone | addone inv | addone | addone diff --git a/src/context/plain.rs b/src/context/plain.rs index 8643ea66..e9e02a26 100644 --- a/src/context/plain.rs +++ b/src/context/plain.rs @@ -213,8 +213,8 @@ impl Context for Plain { // file or in a resource register, so we generate file names for // both cases. let resource = prefix.to_string() + "_" + suffix + ".resource"; - let register = prefix.to_string() + ".register"; - let tag = "<".to_string() + suffix + ">"; + let register = prefix.to_string() + ".md"; + let tag = "```geodesy:".to_string() + suffix + "\n"; for path in &self.paths { // Is it in a separate file? @@ -229,12 +229,13 @@ impl Context for Plain { let mut full_path = path.clone(); full_path.push(section); full_path.push(®ister); - if let Ok(result) = std::fs::read_to_string(full_path) { + if let Ok(mut result) = std::fs::read_to_string(full_path) { + result = result.replace('\r', "\n"); let Some(mut start) = result.find(&tag) else { continue; }; start += tag.len(); - let Some(length) = result[start..].find('<') else { + let Some(length) = result[start..].find("```") else { // Search for end-of-item reached end-of-file let result = result[start..].trim().to_string(); return Ok(result); @@ -346,6 +347,22 @@ mod tests { assert_eq!(data[0][0], 57.); assert_eq!(data[1][0], 61.); + // 3 Console tests from stupid.md + let op = ctx.op("stupid:bad"); + assert!(matches!(op, Err(Error::Syntax(_)))); + + let op = ctx.op("stupid:addthree")?; + let mut data = some_basic_coor2dinates(); + ctx.apply(op, Fwd, &mut data)?; + assert_eq!(data[0][0], 58.); + assert_eq!(data[1][0], 62.); + + let op = ctx.op("stupid:addthree_one_by_one")?; + let mut data = some_basic_coor2dinates(); + ctx.apply(op, Fwd, &mut data)?; + assert_eq!(data[0][0], 58.); + assert_eq!(data[1][0], 62.); + // Make sure we can access "sigil-less runtime defined resources" ctx.register_resource("foo", "bar"); assert!(ctx.get_resource("foo")? == "bar"); diff --git a/src/op/mod.rs b/src/op/mod.rs index ef8c681a..5dfbe9ba 100644 --- a/src/op/mod.rs +++ b/src/op/mod.rs @@ -298,12 +298,12 @@ mod tests { } #[test] - fn macro_expansion_with_defaults_provided() -> Result<(), Error> { + fn macro_expansion_with_defaults() -> Result<(), Error> { let mut data = some_basic_coor2dinates(); let mut ctx = Minimal::default(); // A macro providing a default value of 1 for the x parameter - ctx.register_resource("helmert:one", "helmert x=*1"); + ctx.register_resource("helmert:one", "helmert x=(1)"); // Instantiating the macro without parameters - getting the default let op = ctx.op("helmert:one")?; @@ -339,7 +339,7 @@ mod tests { assert_eq!(data[0][0], 55.); assert_eq!(data[1][0], 59.); - // Overwrite the default, and provide additional args + // Overwrite the default, and provide additional args in a pipeline let op = ctx.op("addone|helmert:one inv x=2")?; ctx.apply(op, Fwd, &mut data)?; @@ -350,6 +350,43 @@ mod tests { assert_eq!(data[0][0], 55.); assert_eq!(data[1][0], 59.); + // A macro providing a default value of 1 for the x parameter, unless + // a macro-parameter called eggs is given + ctx.register_resource("helmert:won", "helmert x=$eggs(1)"); + + // Instantiating the macro without parameters - getting the default + let op = ctx.op("helmert:won")?; + ctx.apply(op, Fwd, &mut data)?; + assert_eq!(data[0][0], 56.); + assert_eq!(data[1][0], 60.); + ctx.apply(op, Inv, &mut data)?; + assert_eq!(data[0][0], 55.); + assert_eq!(data[1][0], 59.); + + // Instantiating the macro with eggs = 2 + let op = ctx.op("helmert:won eggs=2")?; + ctx.apply(op, Fwd, &mut data)?; + assert_eq!(data[0][0], 57.); + assert_eq!(data[1][0], 61.); + ctx.apply(op, Inv, &mut data)?; + assert_eq!(data[0][0], 55.); + assert_eq!(data[1][0], 59.); + + // A macro taking an argument, ham, without any default provided + ctx.register_resource("helmert:ham", "helmert x=$ham"); + + // Instantiating the macro without arguments - getting an error + assert!(matches!(ctx.op("helmert:ham"), Err(Error::Syntax(_)))); + + // Now instantiating the macro with ham = 2 + let op = ctx.op("helmert:ham ham=2")?; + ctx.apply(op, Fwd, &mut data)?; + assert_eq!(data[0][0], 57.); + assert_eq!(data[1][0], 61.); + ctx.apply(op, Inv, &mut data)?; + assert_eq!(data[0][0], 55.); + assert_eq!(data[1][0], 59.); + Ok(()) } diff --git a/src/op/parsed_parameters.rs b/src/op/parsed_parameters.rs index 2c28e274..e4a9d27e 100644 --- a/src/op/parsed_parameters.rs +++ b/src/op/parsed_parameters.rs @@ -419,7 +419,9 @@ pub fn chase( return Ok(Some(String::from(default))); } if chasing { - return Err(Error::Syntax(format!("Incomplete definition for '{key}'"))); + return Err(Error::Syntax(format!( + "Incomplete definition for '{key}' ('{needle}' not found)" + ))); } return Ok(None); } @@ -428,18 +430,33 @@ pub fn chase( // If the value is a(nother) lookup, we continue the search in the same iterator, // now using a *new search key*, as specified by the current value if let Some(stripped) = thevalue.strip_prefix('$') { + let mut parts: Vec<_> = stripped + .trim() + .split(&['(', ')'][..]) + .filter(|x| !x.trim().is_empty()) + .collect(); + if ![1, 2].contains(&parts.len()) { + return Err(Error::Syntax(format!( + "Bad format for optional default in '{thevalue}'" + ))); + } + + // Do we have a default value?, i.e. $arg_name(defualt_value) + if parts.len() == 2 && !chasing { + default = parts.pop().unwrap(); + } chasing = true; - needle = stripped; + needle = parts.pop().unwrap(); continue; } // If the value is a provided default, we continue the search using the *same key*, // in case a proper value is provided. - // cf. the test `macro_expansion_with_defaults_provided()` in `./mod.rs` - if let Some(stripped) = thevalue.strip_prefix('*') { + // cf. the test `macro_expansion_with_defaults_provided_in_parenthesis()` in `./mod.rs` + if let Some(stripped) = thevalue.strip_prefix('(') { chasing = true; needle = key; - default = stripped; + default = stripped.trim_end_matches(')'); continue; } @@ -473,11 +490,12 @@ mod tests { #[test] fn basic() -> Result<(), Error> { + let mut globals = BTreeMap::::new(); + globals.insert("indirection".to_string(), "123".to_string()); + let invocation = String::from( "cucumber flag ellps_0=123 , 456 natural=$indirection sexagesimal=1:30:36 names=alice, bob", ); - let mut globals = BTreeMap::::new(); - globals.insert("indirection".to_string(), "123".to_string()); let raw = RawParameters::new(&invocation, &globals); let p = ParsedParameters::new(&raw, &GAMUT)?; @@ -519,6 +537,7 @@ mod tests { Ellipsoid::new(123., 1. / 456.).semimajor_axis() ); + // Mismatching series format let invocation = String::from("cucumber bad_series=no, numbers, here"); let raw = RawParameters::new(&invocation, &globals); assert!(matches!( @@ -526,6 +545,39 @@ mod tests { Err(Error::BadParam(_, _)) )); + // Invalid indirection (i.e. missing macro argument) + let invocation = String::from("cucumber integer=$not_given"); + let raw = RawParameters::new(&invocation, &globals); + assert!(matches!( + ParsedParameters::new(&raw, &GAMUT), + Err(Error::Syntax(_)) + )); + + // Valid indirection, because we combine the arg with a default + let invocation = String::from("cucumber integer=$not_given_but_defaults_to_42(42)"); + let raw = RawParameters::new(&invocation, &globals); + assert_eq!( + *ParsedParameters::new(&raw, &GAMUT) + .unwrap() + .integer + .get("integer") + .unwrap(), + 42 + ); + + // Valid indirection, because we actually gave the arg at the call point + globals.insert("given_and_is_set_to_43".to_string(), "43".to_string()); + let invocation = String::from("cucumber integer=$given_and_is_set_to_43(42)"); + let raw = RawParameters::new(&invocation, &globals); + assert_eq!( + *ParsedParameters::new(&raw, &GAMUT) + .unwrap() + .integer + .get("integer") + .unwrap(), + 43 + ); + Ok(()) } } diff --git a/src/token/mod.rs b/src/token/mod.rs index e02c1fd8..eb1fcd7a 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -56,11 +56,11 @@ where // Impose some line ending sanity let all = self .as_ref() - .replace("\r\n", "\n") - .replace('\r', "\n") .trim() + .replace("\r\n", "\n") // The fenestration company + .replace('\r', "\n") // The fruit company + .replace("\n:", "\n") // Line continuation markers .to_string(); - // Collect docstrings and remove plain comments let mut trimmed = String::new(); let mut docstring = Vec::::new(); @@ -105,7 +105,7 @@ where fn split_into_parameters(&self) -> BTreeMap { // Remove non-significant whitespace - let step = self.normalize(); + let step = self.as_ref().normalize(); let mut params = BTreeMap::new(); let mut elements: Vec<_> = step.split_whitespace().collect(); if elements.is_empty() { @@ -139,8 +139,13 @@ where } fn normalize(&self) -> String { - let elements: Vec<_> = self.as_ref().split_whitespace().collect(); - elements + // Tweak everything into canonical form + self.as_ref() + .trim() + .trim_matches(':') + .replace("\n:", "\n") + .split_whitespace() + .collect::>() .join(" ") .replace("= ", "=") .replace(": ", ":") @@ -167,6 +172,9 @@ where .replace("₈=", "_8=") .replace("₉=", "_9=") .replace("$ ", "$") // But keep " $" as is! + .split_whitespace() + .collect::>() + .join(" ") } fn is_pipeline(&self) -> bool { @@ -431,7 +439,7 @@ mod tests { // Whitespace agnostic desugaring of '<', '>' into '|omit_fwd', '|omit_inv' assert_eq!( - "foo>bar bar