# Detect Mongolian vowel harmony

The Mongolian language has "vowel harmony". In short the vowel harmony means that certain vowels are not allowed to be placed next to each other in a word. This means that suffixes of words have to care about the vowels present in the words they attach to and need to resolve to a vowel that matches the harmony of the word.

## Vowels

Mongolian harmony compares about 3 binary properties that vowels can have.

• A vowel can be round
• A vowel can be high
• A vowel can be ATR (advanced tongue root)

If a vowel has a property, we indicate that with a + and square brackets if it does not we indicate that with a - and square brackets. e.g. [-ATR] indicates that the vowel does not have the ATR property.

There are thus 8 combinations of these properties that can be made. Mongolian has exactly 1 vowel for each combination except [+high][-ATR][-round] which is missing a vowel.

The 7 vowels and their values can be given in the table below:

[+ATR] [-ATR]
[+round] [+high]

[+round] [-high]

[-round] [+high]

(Absent)
[-round] [-high]

# a (a)

This table gives both the XSAMPA (code block) and IPA (parentheses) values for each vowel, but we will care about the XSAMPA values for this challenge.

## Harmony

### [ATR] harmony

Every word in Mongolian is either [+ATR] or [-ATR] with only vowels of the corresponding [ATR] value appearing in that word. The once exception is i which is "transparent" for [ATR] harmony. Meaning it acts as if it is in both categories even though it is [+ATR]. Meaning that i can appear in words with any vowels.

This can be illustrated with the following Venn diagram:

Modified with permission from Mongolian vowel harmony Venn diagram by wikimedia commons user AquitaneHungerForce. Liscensed under the Creative Commons Attribution-Share Alike 4.0 International license. See link for full attribution.

### [round] harmony

Mongolian also has [round] harmony which only affects [-high] vowels. At its simplest under [round] harmony if a [-high] vowel is the next vowel after a [-high] vowel, it must have the same [round] value.

i is also transparent for roundness harmony so we skip over i when looking for the last vowel.

Given a sequence of Mongolian vowels determine if they follow the rules of vowel harmony. Input can be either a list of XSAMPA vowels, or a string of XSAMPA vowels. Input will always be non-empty.

You should output one of two distinct values, the first if it is a valid sequence the second if it is not.

This is so your goal is to minimize the size of your source code as measured in bytes.

## Test cases

### Valid

iiiiiii
oi
Oi
ui
Ui
ei
ai
UO
oiou
uie
ouie
OUia


### Invalid

uU
oO
eia
oiie
OUie
eieio


This post is based off of the description from:

• Godfrey, Ross (2012). "Opaque Intervention in Khalkha Mongolian Vowel Harmony: A Contrastive Account" pdf.
• Should the test case for ouie fail, as both o and u are [+round], but o is [-high] and u is [+high]? Jun 6, 2022 at 15:27
• @OliverF What is the perceived issue with that case? (It's possible I made an error, but it looks correct to me.) Jun 6, 2022 at 15:32
• Ah I'm sorry, I misread the rules for the round harmony. Jun 6, 2022 at 15:35
• May we take a list of ASCII codes as input? (This may be allowed by default, but I'm not 100% sure about that.) Jun 6, 2022 at 23:37

# JavaScript (Node.js), 45 bytes

Saved 1 byte thanks to @dingledooper

Expects a string. Returns false for valid or true for invalid.

s=>Buffer(s).some(n=>n%7&&s*((s^(s=n))%18%8))


Try it online!

### How?

In this version, we work directly on the ASCII codes of the characters:

 char |  O  |  U  |  a  |  e  |  i  |  o  |  u
------+-----+-----+-----+-----+-----+-----+-----
code |  79 |  85 |  97 | 101 | 105 | 111 | 117


Conveniently, i is the only character whose ASCII code is congruent to $$\0\$$ modulo $$\7\$$.

Once the i's have been filtered out using the above property, a pair of consecutive ASCII codes $$\(x,y)\$$ is valid if and only if:

$$\big((x\operatorname{XOR}y)\bmod 18\big)\bmod 8 = 0$$

Below is a summary of all possible outcomes (using the same order as in my first version for easier comparison):

   | e u O o U a
---+-------------
e | 0 0 6 2 4 4
u | 0 0 4 0 6 2
O | 6 4 0 6 0 2
o | 2 0 6 0 4 6
U | 4 6 0 4 0 0
a | 4 2 2 6 0 0


# JavaScript (ES6), 54 bytes

Expects an array of characters. Returns false for valid or true for invalid.

a=>a.some(c=>(n="ieuOoUa".search(c))&&a*((a^(a=n))%3))


Try it online!

### How?

We map "ieuOoUa" to $$\[0,1,2,3,4,5,6]\$$.

We ignore the i's and consider each pair of indices $$\(x,y)\$$ for the remaining consecutive characters.

The pair is valid if and only if:

$$(x\operatorname{XOR}y)\bmod 3=0$$

Below is a summary of all possible outcomes:

   | e u O o U a
---+-------------
e | 0 0 2 2 1 1
u | 0 0 1 0 1 1
O | 2 1 0 1 0 2
o | 2 0 1 0 1 2
U | 1 1 0 1 0 0
a | 1 1 2 2 0 0

• I think you can save a byte with s*5%9%4 -> s%18%8. Jun 7, 2022 at 3:44
• @dingledooper You're right, thank you! Something went wrong in my search script. Jun 7, 2022 at 7:01
• Save 3 more bytes using [...s] instead of Buffer(s) Jun 9, 2022 at 8:47
• @EduardoPáezRubio That would give a list of characters. I need ASCII codes. Jun 9, 2022 at 8:55

# Jelly, 19 bytes

ḟ”iOµPƝ+⁽D§ẒẸ^+4ẒEƊ


A monadic Link that accepts a list of characters and yields 1 if valid and 0 otherwise.

Try it online! Or see the test-suite.

### How?

Removes the is and then takes the exclusive-or of these two checks using the remaining character's ordinal values:

1. (PƝ+⁽D§ẒẸ) whether any neighbouring pair are from the same [ATR], both [-high], but with different [round] values. That is oe, eo, Oa, or aO.
2. (+4ẒE) whether all are from the same [ATR]

ḟ”iOµPƝ+⁽D§ẒẸ^+4ẒEƊ - Link: list of characters, S (from 'aeoiuOU')
”i                 - an 'i' character
O                - ordinal values
µ               - start a new monadic chain, call that V
Ɲ             - for neighbours in V:
P              -   product
⁽D§         - 18226
Ẓ        - is prime? -> 1 if the neighbours are oe, eo, Oa, or aO
else 0
Ẹ       - any? -> X1
4    -   four
+     -   V add 4 (vectorises)
Ẓ   -   is prime?
E  -   all equal? -> X2
^      - X1 XOR X2


That is:

1. Identifying those neighbours with the same [ATR] where both are [-high] but with different [round] values makes use of this table of ordinal products plus $$\18226\$$ with primes in bold:
x×y+18226 u e o U a O
u 31915 30043 31213 28171 29575 27469
e 30043 28427 29437 26811 28023 26205
o 31213 29437 30547 27661 28993 26995
U 28171 26811 27661 25451 26471 24941
a 29575 28023 28993 26471 27635 25889
O 27469 26205 26995 24941 25889 24467
1. [ATR] groups identified by whether four more than the ordinal is a prime:
ordinal+4 prime?
u 121 no
e 105 no
o 115 no
U 89 yes
a 101 yes
O 83 yes

# Retina, 32 bytes

A[oue]i*[OUa]|oi*e|Oi*a
2}V
.+


### Explanation

A[oue]i*[OUa]|oi*e|Oi*a
2}V


Do the following twice:

• AntiGrep -- Remove lines containing a match for the regex [oue]i*[OUa]|oi*e|Oi*a
• ReVerse the input string

If the input contains any of the invalid combinations, this process will result in an empty string.

.+


Count matches of the regex .+. Outputs 1 if the line is non-empty, 0 otherwise.

• You can use %((G in the header when the code itself contains an unbalanced ) or } (basically you need more (s than unbalanced )s and }s).
– Neil
Jun 6, 2022 at 16:53

# Vyxal, 15 bytes

Returns 0 for valid or 1 for invalid.

\ioC¨pλ꘍∑8%7%;a


Try it Online!

This currently fails for the case iiiiiii due to a bug, but it should work after that gets fixed.

## How?

Ignoring is, the digit sum of the xor of pairwise codepoints is good if it's one of 0, 7, or 8.

   O  U  a  e  o  u
O  0  8 10  6  5 13
U  8  0  7 12 13  5
a 10  7  0  4  5  2
e  6 12  4  0  1  7
o  5 13  5  1  0  8
u 13  5  2  7  8  0

\ioC¨pλ꘍∑8%7%;a
\i              # literal i
o             # remove is
C            # to codepoints
¨pλ      ;  # pairwise map
꘍        # xor
∑       # digit sum
%8     # mod 8
%7   # mod 7
a # are any truthy?

• Bug's been fixed now Jun 22, 2022 at 9:27

# Python, 239 bytes

def f(x):
x=x.replace('i','')
if not x:return 1
a='ueoi';b='UaOi';m='oOea';v='oO'
for c in x:
if(x[0] in a)^(c in a):return-1
r=2
for c in x:
if r!=2:
if(c in m)&r^(c in v):return-1
else:r=2
elif c in m:r=c in v
return 1

Attempt This Online!

A very boring solution, using the most obvious application of the rules.

# Python3, 177 bytes:

lambda s:(S:=set)()in[S(T:=s.replace('i',''))-S('uoe'),S(T)-S('UOa')]and all(T[i:i+2]in'oo,oO,Oo,OO,ee,ea,ae,aa'.split(',')for i in range(len(T)-1)if S()==S(T[i:i+2])-S('oOea'))


Try it online!

• You can save a few bytes using {*'string'} instead of S=set, S('string') Jun 6, 2022 at 22:23

# Jelly, 15 bytes

ḟ”iṢƝ“E“¬(’ḥⱮḂẸ


A monadic Link that accepts a list of characters from aeiouOU and yields 0 if valid and 1 otherwise.

Try it online!

### How?

Beats my other one, but I kind of prefer that one :)

ḟ”iṢƝ“E“¬(’ḥⱮḂẸ - Link: list of characters, S
”i             - an 'i' character
Ɲ           - for each neighbouring pair:
Ṣ            -   sort
Ɱ   - for each sorted pair of neighbours:
ḥ    -   hash using [salt, domain]:
“E“¬(’     -     list of base 250 integers [70, 2041]
Ḃ  - modulo two
Ẹ - any?


# Charcoal, 28 bytes

≔⁺c⁻Ｓiθ›№⁺⌊θ⌈θc⊙⪪oeOa²№⁺⮌θθι


Try it online! Link is to verbose version of code. Outputs a Charcoal boolean, i.e. - if the input is harmonic, nothing if not. Explanation:

≔⁺c⁻Ｓiθ


Remove is from the input, but also prefix a c.

›№⁺⌊θ⌈θc...


Check that c is now either the minimum or maximum of the string, and...

⊙⪪oeOa²№⁺⮌θθι


... that neither the string nor its reversal contain either oe or Oa.

# R, 83 bytes

\(x,g=\(p)grepl(p,x,perl=TRUE))!(g('[ueo]')&g('[UaO]'))&!(g('oi*e|ei*o|Oi*a|ai*O'))

Attempt This Online!

Very straightforward regular expression solution - can't have both sets of vowels, can't have o/e or O/a adjacent with any number of intervening is. (The other combinations are ruled out by [ATR] harmony.)

• -13; -17 by swapping TRUE/FALSE. Jun 7, 2022 at 20:01
• Vast improvements, very nice! Jun 7, 2022 at 21:22
• You can also drop the perl flag, which results in 57 bytes. Jun 8, 2022 at 5:55
• Fantastic, you win! Jun 8, 2022 at 11:58

# Python, 9897 96 bytes

lambda s:len(t:=s.replace("i",""))>1and(y:=ord(t[0]))and any((y^(y:=ord(x)))%18%8for x in t[1:])

Attempt This Online!

-1 byte from @Arnauld altering their answer
-1 byte thanks to @mathjunkie

# Python, 116 115 bytes

lambda s:4in[len({*s}|{*g})for g in["ueoi","UaOi"]]and not any(t in s.replace("i","")for t in["eo","oe","aO","Oa"])

Attempt This Online!

Checks if the set of the characters in the string matches the two valid options (from the Venn diagram) and that none of the illegal combinations ("eo","oe","aO","Oa") are in the "i"-stripped string.

-1 byte thanks to @mathjunkie

• You can remove the space after the 4 Jun 6, 2022 at 23:36
• @mathjunkie I always forget that golf, my ide doesn't like the syntax Jun 6, 2022 at 23:45
• There was a bug in my script that made it miss the shorter %18%8 (found by dingledooper). Jun 7, 2022 at 10:19
• And as pointed out by @mathjunkie, you can now remove the space in 8 for. Jun 7, 2022 at 15:46

# Retina 0.8.2, 38 bytes

i

1[oue][OUa]|[OUa][oue]|oe|eo|Oa|aO


Try it online! Link includes test cases. Outputs 0 for harmonic, 1 if not. Explanation: Retina 0.8.2 doesn't have any handy reversal operations so I have to test for all six non-harmonic vowel combinations.

# 05AB1E, 12 bytes

'iKÇü^18%8ÖP


Port of @Arnauld's first JavaScript answer.
I tried to find a -1 byte alternative for the 18%8Ö using any of 05AB1E's single-byte builtins and constants, but was unable to.

Explanation:

'iK          '# Remove all "i" from the (implicit) input-string
Ç          # Convert the string to a list of codepoint-integers
ü         # For each overlapping pair:
^        #  Bitwise-XOR them together
18%     # Modulo-18 each integer
8Ö   # Check for each integer whether it's divisible by 8
P # Check if all are truthy by taking the product of the list
# (after which the result is output implicitly)
`