7
$\begingroup$

This looks like it should be simple: I want to match an "i" followed by an "o" with an arbitrary (possibly zero-length) sequence of characters in between, unless that sequence also contains an "i". Without this constraint it's easy: ___~~"i"~~___~~"o"~~___. This will match "histology", but it will also match "adventitious". I've tried using a conditional or a predicate test in the pattern but I can't get the syntax right.

$\endgroup$
7
  • $\begingroup$ If there is "i" between them, then this second "i" and "o" will match Your pattern. You have to tell more precisely what do You want. Only one "i" in a string or maybe You do not want to happen "i"~~___~~"i". $\endgroup$
    – Kuba
    Commented Jun 4, 2013 at 11:22
  • $\begingroup$ The pattern should be something like ___ ~~ "i" ~~ Longest[Except["i"] ...] ~~ "o" ~~ ___, but this doesn't work. Someone may correct me, but this may not be expressible as a regular expression (which is a requirement for string patterns). I spent some time a while ago looking for a way to match all except a certain string using regular expressions, eventually finding that it isn't possible. $\endgroup$ Commented Jun 4, 2013 at 11:44
  • $\begingroup$ @OleksandrR. "i" ~~ ___?(! MemberQ[Characters[#], "r"] &) ~~ "o" may work sometimes. I could try to write more general pattern but there is no precise question here. By definition David's problem can not be solved in this form. $\endgroup$
    – Kuba
    Commented Jun 4, 2013 at 12:00
  • $\begingroup$ @OleksandrR. I think the documentation said Longest is the default strategy, so Longest[Except["i"] ...] will not override the Longest on the leading ___, which will be matched first, so will eat any char including "i" until the last one before some "o". $\endgroup$
    – Silvia
    Commented Jun 4, 2013 at 12:32
  • 2
    $\begingroup$ Are you looking to extract the text between "i" and "o", where there are no other "i"s present, or are looking to match an entire word? If the latter, should "adventitious" match, or not? Currently, it meets the conditions implied by your pattern. $\endgroup$
    – rcollyer
    Commented Jun 4, 2013 at 14:12

2 Answers 2

9
$\begingroup$

As commented by Kuba, I'm not sure what is exactly you want, but you might want to try this way:

StringMatchQ[#,
   Except["i"] ... ~~ "i" ~~ Except["i"] ... ~~ "o" ~~ ___
   ] & /@ {"histology", "adventitious"}

{True, False}

Or the regular expression version:

StringMatchQ[#,
   RegularExpression["[^i]*i[^i]*o.*"]
   ] & /@ {"histology", "adventitious"}

{True, False}

Note:

Despite Longest is the default strategy for StringExpression, it seems working differently on List and on String.

  • On List:

    {a, b, c, d, e, f, g} /. {x__, Longest[y__], z__} :> {{x}, {y}, {z}}
    
    {{a}, {b, c, d, e, f}, {g}}
    
    {a, b, c, d, e, f, g} /. {x__, y__, z__} :> {{x}, {y}, {z}}
    
    {{a}, {b}, {c, d, e, f, g}}
    
  • On String:

    StringCases["abcdefg",
     x : __ ~~ Longest[y : __] ~~ z : __ :> {x, y, z}
     ]
    
    {{"abcde", "f", "g"}}
    
    StringCases["abcdefg",
     x : __ ~~ y : __ ~~ z : __ :> {x, y, z}
     ]
    
    {{"abcde", "f", "g"}}
    

So you'll have to indicate explicitly that the leading part should not contain "i" (so Except["i"] ... instead of ___) to enforce the middle part "i" ~~ Except["i"] ... ~~ "o" actually matching the longest pattern. I personally guess that is because string match is greedy and the leading part is matched before the middle part.

Supplement:

There is a great free tool for regex debugging, which can show the matching step by step. Also a useful online tester. (I learnt them from this post.)

$\endgroup$
12
  • $\begingroup$ It should be noted that RegularExpression["[^i]*i[^i]*o.*"] does match part of "adventitious" and using it with StringCases will return "tious". Of course, I interpret the OPs original pattern to match "i[^i]*o". $\endgroup$
    – rcollyer
    Commented Jun 4, 2013 at 12:44
  • $\begingroup$ @rcollyer Yes it is. I think it because StringMatchQ compares the whole string with the pattern, while StringCases will compare all sub-string. I interpret what OP wants based on the statement: an "i" followed by an "o" with an arbitrary (possibly zero-length) sequence of characters in between, unless that sequence also contains an "i". $\endgroup$
    – Silvia
    Commented Jun 4, 2013 at 12:50
  • 1
    $\begingroup$ @OleksandrR. Thanks. I think it is because the a~~b~~c pattern divides a whole string, e.g. "adventitious", into 3 parts, in which part b must NOT have "i", so even part a is shortest, it has to extend to include all "i"s before the part c. Adding a ___ between a and b will solve the problem. In regex form, it's "^(.*?i).*([^i]*)(o.*?)$" instead of "^(.*?i)([^i]*)(o.*?)$". $\endgroup$
    – Silvia
    Commented Jun 4, 2013 at 17:53
  • 1
    $\begingroup$ @OleksandrR. There is a great free tool for regex debugging, which can show the matching step by step. Also a useful online tester. Learnt them from this post. $\endgroup$
    – Silvia
    Commented Jun 4, 2013 at 17:57
  • 1
    $\begingroup$ @OleksandrR. A StringExpression version: StringCases[{"histology","adventitious","pious","bilious"},a:Shortest[StartOfString~~___~~"i"]~~b:Shortest[___]~~c:Except["i"]...~~d:Shortest["o"~~___~~EndOfString]:>Sequence@@{a,b,c,d}]//Grid[#,Frame->All]&. $\endgroup$
    – Silvia
    Commented Jun 4, 2013 at 18:03
0
$\begingroup$

Using SequenceCases (new in 10.1)

f = SequenceCases[Characters[#], {"i", Except["i" | "o"] .., "o"}] =!= {} &

f /@ {"histology", "adventitious"}

{True, False}

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.