# Ionic Compound Golf

## Challenge

Given two inputs, a positive ion and a negative ion, you must output the formula for the ionic compound which would be made from the two ions. This basically means balancing out the charges so they equal zero.

Do not bother with formatting the formula with subscript numbers, but you must have brackets for the multi-atom ions (such as NO3).

You do not have to account for any errors (for example, if someone inputs two negative ions, you can just let the program fail).

Note: Take Fe to have a charge of 3+

## Ions

All of the ions which need to be accounted for are found along with their charges on the second part of the AQA GCSE Chemistry Data Sheet.

• H+

• Na+

• Ag+

• K+

• Li+

• NH4+

• Ba2+

• Ca2+

• Cu2+

• Mg2+

• Zn2+

• Pb2+

• Fe3+

• Al3+

• Cl-

• Br-

• F-

• I-

• OH-

• NO3-

• O2-

• S2-

• SO42-

• CO32-

## Examples

Some examples:

H and O returns:- H2O

Ca and CO3 returns:- CaCO3

Al and SO4 returns:- Al2(SO4)3

Note the following case that you must account for:

H and OH returns:- H2O not H(OH)

• Is the positive ion always listed first?
– xnor
Commented Jun 25, 2015 at 8:18
• Does Fe have a charge of 2+ or 3+? Commented Jun 25, 2015 at 8:18
• @xnor No, they can switch Commented Jun 25, 2015 at 8:19
• This reminds me of my chemistry class I had one year ago. Balancing stuff... Commented Jun 25, 2015 at 8:36
• I assume brackets must be used when necessary and only when necessary. Correct? Commented Jun 25, 2015 at 11:45

# Lua, 174 242 bytes

I forgot the brackets '-_-, that got me up to 242. Oh well, it was a fun enough challenge at least.

i,c,a={Ba=2,Ca=2,Cu=2,Mg=2,Zn=2,Pb=2,Fe=3,Al=3,O=2,S=2,SO4=2,CO3=2},io.read(),io.read()
p,d=i[c]~=i[a],{SO4=1,NO3=1,OH=1,CO3=1}
k,m=p and i[c]or'',p and i[a]or''
a=k==m and a or (d[a]and'('..a..')'or a)
print(c..a=='HOH'and'H2O'or c..m..a..k)


Try it online!

Old version:

i,c,a={Ba=2,Ca=2,Cu=2,Mg=2,Zn=2,Pb=2,Fe=3,Al=3,O=2,S=2,SO4=2,CO3=2},io.read(),io.read()
p=i[c]~=i[a]
k,m=p and i[c]or'',p and i[a]or''
print(c..a=='HOH'and'H2O'or c..m..a..k)


Abusing Lua's tendency to initialize everything with a nil value we can cut down on storage costs. Still, Lua is still a bit clunky :(

• Your output should support having parentheses in it. So Al and SO4 should output Al2(SO4)3, but you output Al2SO43. Try it online Commented May 19, 2017 at 20:12
• Yeah, only realized that some 5 minutes after I hit send. '-_-. It should work now. Commented May 19, 2017 at 20:14
• Now it outputs parens when it doesn't need to. Try Ca and CO3. Also, you should add the TIO link to your answer. Commented May 19, 2017 at 20:17
• There! Sorry, I didn't know about the TIO thing. My bad. Commented May 19, 2017 at 20:28
• It's just helpful for users wanting to run your code. Commented May 19, 2017 at 20:35

# Java (619647 667 bytes)

[Fixed] Update: H + OH returns HOH even though I hard coded it not to.... working on it

[Fixed] Update: Sometimes Parenthesis appear when they shouldn't

Code

String f(String[]a){if(Arrays.equals(a,new String[]{"H","OH"})|Arrays.equals(a,new String[]{"OH","H"}))return "H2O";List<String>b=Arrays.asList(new String[]{"H","Na","Ag","K","Li","NH4","Ba","Ca","Cu","Mg","Zn","Pb","Fe","Al","Cl","Br","F","I","OH","NO3","O","S","SO4","CO3"});Integer[]c={1,1,1,1,1,1,2,2,2,2,2,2,3,3,1,1,1,1,1,1,2,2,2,2},d={5,18,19,22,23};List<Integer>j=Arrays.asList(d);int e=b.indexOf(a[0]),f=b.indexOf(a[1]),g=c[e],h=c[f],i;if(f<e){String p=a[0];a[0]=a[1];a[1]=p;i=g;g=h;h=i;i=e;e=f;f=i;}boolean k=j.contains(e),l=j.contains(f),m=g==h,n=g==1,o=h==1;return (k&!m&!o?"("+a[0]+")":a[0])+(m?"":h==1?"":h)+(l&!m&!n?"("+a[1]+")":a[1])+(m?"":g==1?"":g);}


I wasn't sure how to do this without hard coding every ions charge, so it ended up being long. Lucky cause all the charges are 1, 2, or 3 so finding the amount of each ion is easy.

Expanded

import java.util.Arrays;
import java.util.List;
public class Compound {
public static void main(String[]a){
//System.out.println(f(a));
String[] pos = new String[]{"H","Na","Ag","K","Li","NH4","Ba","Ca","Cu","Mg","Zn","Pb","Fe","Al"};
String[] neg = new String[]{"Cl","Br","F","I","OH","NO3","O","S","SO4","CO3"};
for(int i = 0; i < pos.length; i++){
for(int j = 0; j < neg.length; j++){
System.out.println(pos[i] + " + " + neg[j] + " = " + f(new String[]{pos[i],neg[j]}));
System.out.println(neg[j] + " + " + pos[i] + " = " + f(new String[]{neg[j],pos[i]}));
}
}
}
static String f(String[]a){
if(Arrays.equals(a,new String[]{"H","OH"})|Arrays.equals(a,new String[]{"OH","H"}))
return "H2O";
List<String>b=Arrays.asList(new String[]{"H","Na","Ag","K","Li","NH4","Ba","Ca","Cu","Mg","Zn","Pb","Fe","Al","Cl","Br","F","I","OH","NO3","O","S","SO4","CO3"});
Integer[]c={1,1,1,1,1,1,2,2,2,2,2,2,3,3,1,1,1,1,1,1,2,2,2,2},d={5,18,19,22,23};
List<Integer>j=Arrays.asList(d);
int e=b.indexOf(a[0]),f=b.indexOf(a[1]),g=c[e],h=c[f],i;
if(f<e){String p=a[0];a[0]=a[1];a[1]=p;i=g;g=h;h=i;i=e;e=f;f=i;}
boolean k=j.contains(e),l=j.contains(f),m=g==h,n=g==1,o=h==1;
return (k&!m&!o?"("+a[0]+")":a[0])+(m?"":o?"":h)+(l&!m&!n?"("+a[1]+")":a[1])+(m?"":n?"":g);
}
}


Try it here

Data

Let me know if any of them are wrong

H + Cl = HCl
Cl + H = HCl
H + Br = HBr
Br + H = HBr
H + F = HF
F + H = HF
H + I = HI
I + H = HI
H + OH = H2O
OH + H = H2O
H + NO3 = HNO3
NO3 + H = HNO3
H + O = H2O
O + H = H2O
H + S = H2S
S + H = H2S
H + SO4 = H2SO4
SO4 + H = H2SO4
H + CO3 = H2CO3
CO3 + H = H2CO3
Na + Cl = NaCl
Cl + Na = NaCl
Na + Br = NaBr
Br + Na = NaBr
Na + F = NaF
F + Na = NaF
Na + I = NaI
I + Na = NaI
Na + OH = NaOH
OH + Na = NaOH
Na + NO3 = NaNO3
NO3 + Na = NaNO3
Na + O = Na2O
O + Na = Na2O
Na + S = Na2S
S + Na = Na2S
Na + SO4 = Na2SO4
SO4 + Na = Na2SO4
Na + CO3 = Na2CO3
CO3 + Na = Na2CO3
Ag + Cl = AgCl
Cl + Ag = AgCl
Ag + Br = AgBr
Br + Ag = AgBr
Ag + F = AgF
F + Ag = AgF
Ag + I = AgI
I + Ag = AgI
Ag + OH = AgOH
OH + Ag = AgOH
Ag + NO3 = AgNO3
NO3 + Ag = AgNO3
Ag + O = Ag2O
O + Ag = Ag2O
Ag + S = Ag2S
S + Ag = Ag2S
Ag + SO4 = Ag2SO4
SO4 + Ag = Ag2SO4
Ag + CO3 = Ag2CO3
CO3 + Ag = Ag2CO3
K + Cl = KCl
Cl + K = KCl
K + Br = KBr
Br + K = KBr
K + F = KF
F + K = KF
K + I = KI
I + K = KI
K + OH = KOH
OH + K = KOH
K + NO3 = KNO3
NO3 + K = KNO3
K + O = K2O
O + K = K2O
K + S = K2S
S + K = K2S
K + SO4 = K2SO4
SO4 + K = K2SO4
K + CO3 = K2CO3
CO3 + K = K2CO3
Li + Cl = LiCl
Cl + Li = LiCl
Li + Br = LiBr
Br + Li = LiBr
Li + F = LiF
F + Li = LiF
Li + I = LiI
I + Li = LiI
Li + OH = LiOH
OH + Li = LiOH
Li + NO3 = LiNO3
NO3 + Li = LiNO3
Li + O = Li2O
O + Li = Li2O
Li + S = Li2S
S + Li = Li2S
Li + SO4 = Li2SO4
SO4 + Li = Li2SO4
Li + CO3 = Li2CO3
CO3 + Li = Li2CO3
NH4 + Cl = NH4Cl
Cl + NH4 = NH4Cl
NH4 + Br = NH4Br
Br + NH4 = NH4Br
NH4 + F = NH4F
F + NH4 = NH4F
NH4 + I = NH4I
I + NH4 = NH4I
NH4 + OH = NH4OH
OH + NH4 = NH4OH
NH4 + NO3 = NH4NO3
NO3 + NH4 = NH4NO3
NH4 + O = (NH4)2O
O + NH4 = (NH4)2O
NH4 + S = (NH4)2S
S + NH4 = (NH4)2S
NH4 + SO4 = (NH4)2SO4
SO4 + NH4 = (NH4)2SO4
NH4 + CO3 = (NH4)2CO3
CO3 + NH4 = (NH4)2CO3
Ba + Cl = BaCl2
Cl + Ba = BaCl2
Ba + Br = BaBr2
Br + Ba = BaBr2
Ba + F = BaF2
F + Ba = BaF2
Ba + I = BaI2
I + Ba = BaI2
Ba + OH = Ba(OH)2
OH + Ba = Ba(OH)2
Ba + NO3 = Ba(NO3)2
NO3 + Ba = Ba(NO3)2
Ba + O = BaO
O + Ba = BaO
Ba + S = BaS
S + Ba = BaS
Ba + SO4 = BaSO4
SO4 + Ba = BaSO4
Ba + CO3 = BaCO3
CO3 + Ba = BaCO3
Ca + Cl = CaCl2
Cl + Ca = CaCl2
Ca + Br = CaBr2
Br + Ca = CaBr2
Ca + F = CaF2
F + Ca = CaF2
Ca + I = CaI2
I + Ca = CaI2
Ca + OH = Ca(OH)2
OH + Ca = Ca(OH)2
Ca + NO3 = Ca(NO3)2
NO3 + Ca = Ca(NO3)2
Ca + O = CaO
O + Ca = CaO
Ca + S = CaS
S + Ca = CaS
Ca + SO4 = CaSO4
SO4 + Ca = CaSO4
Ca + CO3 = CaCO3
CO3 + Ca = CaCO3
Cu + Cl = CuCl2
Cl + Cu = CuCl2
Cu + Br = CuBr2
Br + Cu = CuBr2
Cu + F = CuF2
F + Cu = CuF2
Cu + I = CuI2
I + Cu = CuI2
Cu + OH = Cu(OH)2
OH + Cu = Cu(OH)2
Cu + NO3 = Cu(NO3)2
NO3 + Cu = Cu(NO3)2
Cu + O = CuO
O + Cu = CuO
Cu + S = CuS
S + Cu = CuS
Cu + SO4 = CuSO4
SO4 + Cu = CuSO4
Cu + CO3 = CuCO3
CO3 + Cu = CuCO3
Mg + Cl = MgCl2
Cl + Mg = MgCl2
Mg + Br = MgBr2
Br + Mg = MgBr2
Mg + F = MgF2
F + Mg = MgF2
Mg + I = MgI2
I + Mg = MgI2
Mg + OH = Mg(OH)2
OH + Mg = Mg(OH)2
Mg + NO3 = Mg(NO3)2
NO3 + Mg = Mg(NO3)2
Mg + O = MgO
O + Mg = MgO
Mg + S = MgS
S + Mg = MgS
Mg + SO4 = MgSO4
SO4 + Mg = MgSO4
Mg + CO3 = MgCO3
CO3 + Mg = MgCO3
Zn + Cl = ZnCl2
Cl + Zn = ZnCl2
Zn + Br = ZnBr2
Br + Zn = ZnBr2
Zn + F = ZnF2
F + Zn = ZnF2
Zn + I = ZnI2
I + Zn = ZnI2
Zn + OH = Zn(OH)2
OH + Zn = Zn(OH)2
Zn + NO3 = Zn(NO3)2
NO3 + Zn = Zn(NO3)2
Zn + O = ZnO
O + Zn = ZnO
Zn + S = ZnS
S + Zn = ZnS
Zn + SO4 = ZnSO4
SO4 + Zn = ZnSO4
Zn + CO3 = ZnCO3
CO3 + Zn = ZnCO3
Pb + Cl = PbCl2
Cl + Pb = PbCl2
Pb + Br = PbBr2
Br + Pb = PbBr2
Pb + F = PbF2
F + Pb = PbF2
Pb + I = PbI2
I + Pb = PbI2
Pb + OH = Pb(OH)2
OH + Pb = Pb(OH)2
Pb + NO3 = Pb(NO3)2
NO3 + Pb = Pb(NO3)2
Pb + O = PbO
O + Pb = PbO
Pb + S = PbS
S + Pb = PbS
Pb + SO4 = PbSO4
SO4 + Pb = PbSO4
Pb + CO3 = PbCO3
CO3 + Pb = PbCO3
Fe + Cl = FeCl3
Cl + Fe = FeCl3
Fe + Br = FeBr3
Br + Fe = FeBr3
Fe + F = FeF3
F + Fe = FeF3
Fe + I = FeI3
I + Fe = FeI3
Fe + OH = Fe(OH)3
OH + Fe = Fe(OH)3
Fe + NO3 = Fe(NO3)3
NO3 + Fe = Fe(NO3)3
Fe + O = Fe2O3
O + Fe = Fe2O3
Fe + S = Fe2S3
S + Fe = Fe2S3
Fe + SO4 = Fe2(SO4)3
SO4 + Fe = Fe2(SO4)3
Fe + CO3 = Fe2(CO3)3
CO3 + Fe = Fe2(CO3)3
Al + Cl = AlCl3
Cl + Al = AlCl3
Al + Br = AlBr3
Br + Al = AlBr3
Al + F = AlF3
F + Al = AlF3
Al + I = AlI3
I + Al = AlI3
Al + OH = Al(OH)3
OH + Al = Al(OH)3
Al + NO3 = Al(NO3)3
NO3 + Al = Al(NO3)3
Al + O = Al2O3
O + Al = Al2O3
Al + S = Al2S3
S + Al = Al2S3
Al + SO4 = Al2(SO4)3
SO4 + Al = Al2(SO4)3
Al + CO3 = Al2(CO3)3
CO3 + Al = Al2(CO3)3


Note

I started in Pyth, but then I got annoyed with the order and the parenthesis, here is what I had if anyone wants to finish it.

=G["H" "Na" "Ag" "K" "Li" "NH4" "Ba" "Ca" "Cu" "Mg" "Zn" "Pb" "Fe" "Al" "Cl" "Br" "F" "I" "OH" "NO3" "O" "S" "SO4" "CO3" 1 1 1 1 1 1 2 2 2 2 2 2 3 3 1 1 1 1 1 1 2 2 2 2)J@G+24xG@QZK@G+24xG@Q1@QZ?kqJKK@Q1?kqJKJ

• What was the problem with H and OH? Commented Jun 26, 2015 at 15:30
• It's fixed, before it was returning H + OH = HOH not H2O
– cmxu
Commented Jun 26, 2015 at 15:34
• I mean what was it that was making it return HOH instead of H20? Commented Jun 26, 2015 at 15:40
• Looks like there are some unneeded parentheses, at least the way I read the rules. For example, in H + SO4, I think the result should be H2SO4, without parentheses. Commented Jun 26, 2015 at 15:41
• @RetoKoradi I noticed right before I read your comment, but it's been fixed now. Thanks.
– cmxu
Commented Jun 26, 2015 at 15:43

# CJam, 176 bytes

"O S SO4 CO3 ""Cl Br F I OH NO3 """"H Na Ag K Li NH4 ""Ba Ca Cu Mg Zn Pb ""Fe Al "]_2{r\1$S+f#Wf=0#((z@}*_2$*_4=2*-_@/\@/@\]2/{~_({1$1>_el={'(@@')\}|0}&;}/]s"HOHH"1$#){;"H2O"}&


Try it online

This was somewhat painful, particularly with all the special cases for parentheses, showing counts, the H2O, etc.

The data is not in a super compact format. It could be trimmed more, but the code needed to interpret it would probably compensate for the savings. So I went with an array of strings, where each string contains the atoms with the same charge, ordered from -2 to +3 (where the string for 0 is empty).

Explanation:

[..]  Data, as explained above.
_     Duplicate data, will need it for both inputs.
2{    Loop over two inputs.
r     Get input.
\     Swap data to top.
1$Copy input to top (keep original for later). S+ Add a space to avoid ambiguity when searching in data. f# Search for name in all strings of data. Wf= Convert search results to truth values by comparing them to -1. 0# Find the 0 entry, which gives the index of the matching string. (( Subtract 2, to get charge in range [-2, 3] from index. z Absolute value, we don't really care about sign of charge. @ Swap second copy of data table to proper position for next input. }* End loop over two inputs. _2$*  Multiply the two charges.
_4=   We need the LCM. But for the values here, only product 4 is not the LCM.
2*-   So change it to 2.
_@/   Divide LCM by first charge to get first count.
\@/   Divide LCM by second charge to get second count.
@\]2/ Make pairs of name/count for both ions...
{     ... and loop over the pairs.
~     Unpack the pair.
_(    Check if count is > 1.
{       Handle count > 1.
1$Get copy of name to top of stack. 1> Slice off first character to check if rest contains upper case. _el Create lower case copy. = If they are different, there are upper case letters. { Handle case where parentheses are needed. '( Opening parentheses. @@ Some stack shuffling to get values in place. ') Closing parentheses. \ And one more swap to place count after parentheses. }| End parentheses handling. 0 Push dummy value to match stack layout of other branch. }& End count handling. ; Pop unused count off stack. }/ End loop over name/count pairs. ]s Pack stack content into single string, for H2O handling. "HOHH" String that contains both HOH and OHH, which need to be H2O. 1$#)  Check if output is in that string.
{     If yes, replace with H2O.
;     Drop old value.
"H2O" And make it H2O instead.
}&    End of H2O handling.


## JavaScript (ES6), 316 277 bytes

I've globalized the p and n variables (just like I did in CoffeeScript) for easier testing. Localizing the variables would not make a difference in character count.

f=(x,y)=>{i='indexOf',d='~NH4KNaAgLiBa~CaCuMgZnPbFeAlSO4CO3ClBrFIOHNO3',k=d[i](x),l=d[i](y),a=k<28?x:y,b=k<28?y:x,r=Math.ceil(k/11)-1,s=b=='F'?0:l<32;if(r==s)r=s=0;a=s&&a=='NH4'?'(NH4)':a;b=r&&/[A-Z]{2}/.test(b)?(${b}):b;return'H'==a&&b=='OH'?'H2O':a+(s?s+1:'')+b+(r?r+1:'')} // Original attempt, 316 bytes p={H:1,Na:1,Ag:1,K:1,Li:1,NH4:1,Ba:2,Ca:2,Cu:2,Mg:2,Zn:2,Pb:2,Fe:3,Al:3},n={Cl:1,Br:1,F:1,I:1,OH:1,NO3:1,O:2,S:2,SO4:2,CO3:2},f=(x,y)=>{a=p[x]?x:y,b=p[x]?y:x,z=p[a]==2&&n[b]==2,r=+z||n[b],s=+z||p[a];a=--r&&a=='NH4'?'(NH4)':a;b=--s&&/[A-Z]{2}/.test(b)?(${b}):b;return'H'==a&&b=='OH'?'H2O':a+(r?r+1:'')+b+(s?s+1:'')}


### ES5 variant, 323 284 bytes

Not much changes apart from getting rid of the arrow function and template string:

f=function(x,y){i='indexOf',d='~NH4KNaAgLiBa~CaCuMgZnPbFeAlSO4CO3ClBrFIOHNO3',k=d[i](x),l=d[i](y),a=k<28?x:y,b=k<28?y:x,r=Math.ceil(k/11)-1,s=b=='F'?0:l<32;if(r==s)r=s=0;a=s&&a=='NH4'?'(NH4)':a;b=r&&/[A-Z]{2}/.test(b)?'('+b+')':b;return'H'==a&&b=='OH'?'H2O':a+(s?s+1:'')+b+(r?r+1:'')}

// Original attempt, 323 bytes
p={H:1,Na:1,Ag:1,K:1,Li:1,NH4:1,Ba:2,Ca:2,Cu:2,Mg:2,Zn:2,Pb:2,Fe:3,Al:3},n={Cl:1,Br:1,F:1,I:1,OH:1,NO3:1,O:2,S:2,SO4:2,CO3:2},f=function(x,y){a=p[x]?x:y,b=p[x]?y:x,z=p[a]==2&&n[b]==2,r=+z||n[b],s=+z||p[a];a=--r&&a=='NH4'?'(NH4)':a;b=--s&&/[A-Z]{2}/.test(b)?'('+b+')':b;return'H'==a&&b=='OH'?'H2O':a+(r?r+1:'')+b+(s?s+1:'')}


f=function(x,y){i='indexOf',d='~NH4KNaAgLiBa~CaCuMgZnPbFeAlSO4CO3ClBrFIOHNO3',k=d[i](x),l=d[i](y),a=k<28?x:y,b=k<28?y:x,r=Math.ceil(k/11)-1,s=b=='F'?0:l<32;if(r==s)r=s=0;a=s&&a=='NH4'?'(NH4)':a;b=r&&/[A-Z]{2}/.test(b)?'('+b+')':b;return'H'==a&&b =='OH'?'H2O':a+(s?s+1:'')+b+(r?r+1:'')}

;['H', 'Na', 'Ag', 'K', 'Li', 'NH4', 'Ba', 'Ca', 'Cu', 'Mg', 'Zn', 'Pb', 'Fe', 'Al'].forEach(function (q) {
['Cl', 'Br', 'F', 'I', 'OH', 'NO3', 'O', 'S', 'SO4', 'CO3'].forEach(function (w) {
document.body.innerHTML += '<p>' + q + ' + ' + w + ' = ' + f(q, w);
});
});

## CoffeeScript, 371 333 bytes

This count includes newlines (some newlines can be replaced with semicolons but that wouldn't affect character count)

f=(x,y)->(i='indexOf';d='~NH4KNaAgLiBa~CaCuMgZnPbFeAlSO4CO3ClBrFIOHNO3';k=d[i] x;r=-1+Math.ceil k/11;l=d[i] y;a=if k<28then x else y
b=if k<28then y else x
s=if b=='F'then 0else 32>l
r=s=0if r==s;a='(NH4)'if'NH4'==a&&s;b='('+b+')'if/[A-Z]{2}/.test(b)&&r;if'H'==a&&b=='OH'then'H2O'else a+(if!s then''else 1+s)+b+(if!r then''else 1+r))


# Original attempt, 371 bytes
p={H:1,Na:1,Ag:1,K:1,Li:1,NH4:1,Ba:2,Ca:2,Cu:2,Mg:2,Zn:2,Pb:2,Fe:3,Al:3}
n={Cl:1,Br:1,F:1,I:1,OH:1,NO3:1,O:2,S:2,SO4:2,CO3:2}
f=(x,y)->(a=if p[x]then x else y
b=if p[x]then y else x
z=p[a]==n[b]==2;r=+z||n[b];s=+z||p[a];if--r&&a=='NH4'then a='(NH4)'
if--s&&/[A-Z]{2}/.test(b)then b='('+b+')'
if'H'==a&&b=='OH'then'H2O'else a+(if!r then''else r+1)+b+(if!s then''else s+1))


## CJam (137 bytes)

{{"SO4CO3ClBrFINO3OHNaAgKLiNH4BaCaCuMgZnPbFeAl":I\#~}$_s"HOH"="HO"1/@?_{I\#G-_0<B*-B/_W>+z}%_~1$=\1?f/W%[.{:T1>{_{'a<},,1>{'(\')}*T}*}]s}


This is an anonymous block (function) which takes two ions as strings wrapped in a list, and returns a string.

### Dissection

{                        e# Begin a block
{                      e#   Sort the input list
"SO4...Al"           e#     Ions in ascending order of charge
:I                   e#     Stored as I for future reuse
\#~                  e#     Index and bit-invert to sort descending
}$_s"HOH"="HO"1/@? e# Special case for water: replace ["H" "OH"] with ["H" "O"] _{I\#G-_0<B*-B/_W>+z}% e# Copy the list and hash index in I to find the charges _~1$=\1?f/             e#   Replace [2 2] by [1 1]
W%                     e#   Reverse the charges
[                      e#   Gather in an array
.{                   e#   Pairwise for each ion and its opponent's reduced charge...
:T1>{              e#     If the charge (copied to T) is greater than 1
_{'a<},,         e#       Count the characters in the ion which are before 'a'
1>{'(\')}*       e#       If there's more than one, add some parentheses
T                e#       Append T
}*
}
]
s                      e#   Flatten the list to a single string
}


# Python 3, 364 bytes

Stores the ions by the absolute value of their charges as index+1 in a 2-D array (0-index elements have +- 1 charge, etc.). Uses string.split() to save a few characters there. Handles the special case of H + OH = H2O first, then it calculates how many ions are needed of each sort as the LCM of their two charges divided by its charge. Then adds in parentheses if needed as well as the actual number of ions.

from math import*
o=["NH4 NO3 H Na Ag K Li OH I F Br Cl".split(),"CO3 SO4 Ba Ca Cu Mg Zn Pb S O".split(),["Fe","Al"]]
p=["H","OH"]
def c(i):
for h,k in enumerate(o):
if i in k:return-~h
def b(x,y):
if x in p and y in p:return"H20"
i,j=c(x),c(y);g=gcd(i,j);i//=g;j//=g;return((x,"(%s)"%x)[x[-1]in"34"]+str(j),x)[j==1]+((y,"(%s)"%y)[y[-1]in"34"]+str(i),y)[i==1]


Try it online!