7

I have hundreds of text files that include chemical formulas together with narrative including numerical values. The formulas are always preceded by white spaces but can be followed by white spaces, commas, periods, etc.

The problem is: the formulas are not formatted to display numbers as subscripts e.g.:

H2SO4, C5H11OH.

I want to format the subscripts as HTML tags, e.g.:

H<sub>2</sub>SO<sub>4</sub>, C<sub>5</sub>H<sub>11</sub>OH

So that subscripts render in HTML, e.g.:

H2SO4, C5H11OH

I have toyed with accomplishing this with Java, php, etc., but the implementations are necessarily messy and awkward. I suspect that there is an elegant sed/awk approach.

Clearly, part of the solution is to craft a regular expression that matches a letter followed by one or more digits as a formula detection mechanism (there may be false positives that I will manually correct later). Then, given a formula so identified, a sed replacement needs to precede each digit or sequence of digits with the sub tag and follow it with a sub tag closure.

There must be a one-liner that does this, but I'm over my head.

Any ideas?

1
  • So, is there a reason you added an image rather than an inline code block? See more formatting help: help center
    – cat
    Apr 22, 2016 at 1:41

3 Answers 3

4

E.g.:

sed -r 's:([A-Za-z])([0-9]+):\1<sub>\2</sub>:g'  

should do the job.

(Match a letter followed by a group of digits and remember it as \1 and \2. Replace all of that by the same letter (\1) plus the digit group (\2) enclosed in the sub tag.)

3

Since you mention there may be false positives to manually correct later, you may want to consider a slightly more robust form which incorporates the following restrictions:

  1. All chemical symbols start with an uppercase letter.
  2. All chemical symbols are either a single uppercase letter or a single uppercase letter followed by a single lower case letter, excepting only temporary designators which I will ignore.

Given these you could try, for instance:

sed 's|\([[:upper:]][[:lower:]]\{0,1\}\)\([0-9]\{1,\}\)|\1<sub>\2</sub>|g'

With the non-POSIX -r option this becomes slightly more readable but less portable:

sed -r 's|([[:upper:]][[:lower:]]?)([0-9]+)|\1<sub>\2</sub>|g'

This could be improved still further by ensuring that the entire "word" being worked on contained no consecutive lowercase letters, and of course could be improved even further by checking specifically for each possible chemical symbol, but that gets fancier and fancier for less payoff. The above should drastically reduce false positives already.

5
  • 2
    As per unix.stackexchange.com/questions/267148/… you can use -E instead of -r and remain "POSIX-y".
    – Dani_l
    Apr 22, 2016 at 7:01
  • It's not less portable - it's just as portable as the first one is (that is, not portable; for portable BRE see OP's answer). Apr 22, 2016 at 16:03
  • @don_crissti, which part of my first command is non-POSIX?
    – Wildcard
    Apr 22, 2016 at 17:32
  • 1
    \? and \+ ... gnu sed and other apps/implementations are "smart" so they understand those qualifiers even if - per the standard - they're not BRE. Apr 22, 2016 at 17:45
  • @don_crissti, thanks; I never realized that. I see that it doesn't work in BSD sed as originally written. I've now fixed it to use POSIX specified quantifiers.
    – Wildcard
    Apr 22, 2016 at 19:58
2

Grouping and back references were the trick. Thanks for the push in the right direction. In the end, I used the following:

sed 's/\([A-Z][a-z]*\)\([0-9][0-9]*\)/\1<sub>\2<\/sub>/g' file

This tolerates the cases where a header, e.g. h2, occurs in the document.

0

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .