Convert a percentage to a “simple” ratio

You run a political website, and have determined that people have a better intuitive understanding when the chance of winning or losing an election is expressed as a ratio ("5 in 7") than when it is expressed as a percentage ("71%").

But you also don't want to display confusing ratios like "58 in 82", you'd like them to be more easily understood, even if they aren't quite as precise.

So, given a percentage between 0.1% and 99.9%, return the closest "easy-to-understand" ratio "x in y", using the following rules:

1. Most values (see exceptions below) should return the closest ratio out of 10 or lower. 55% should return "5 in 9", not "11 in 20".
2. Ratios should be reduced to their lowest terms. 65% should return "2 in 3", not "4 in 6".
3. Values under 10% should return the closest ratio of the form "1 in n" where n is one of (10,12,15,20,30,40,50,60,70,80,90,100). For example, 6% should return "1 in 15".
4. Values over 90% should return the closest ratio of the form "n-1 in n" where n is one of (10,12,15,20,30,40,50,60,70,80,90,100). For example, 98.7% should return "79 in 80".
5. Values under 1% should return "<1 in 100"
6. Values over 99% should return ">99 in 100"

Or, to think about it another way, your program should return the closest ratio from the following possible outputs (I've included their approximate values for your convenience):

<1 in 100
1 in 100  = 1.00%
1 in 90   = 1.11%
1 in 80   = 1.25%
1 in 70   = 1.43%
1 in 60   = 1.67%
1 in 50   = 2.00%
1 in 40   = 2.50%
1 in 30   = 3.33%
1 in 20   = 5.00%
1 in 15   = 6.67%
1 in 12   = 8.33%
1 in 10   = 10.00%
1 in 9    = 11.11%
1 in 8    = 12.50%
1 in 7    = 14.29%
1 in 6    = 16.67%
1 in 5    = 20.00%
2 in 9    = 22.22%
1 in 4    = 25.00%
2 in 7    = 28.57%
3 in 10   = 30.00%
1 in 3    = 33.33%
3 in 8    = 37.50%
2 in 5    = 40.00%
3 in 7    = 42.86%
4 in 9    = 44.44%
1 in 2    = 50.00%
5 in 9    = 55.56%
4 in 7    = 57.14%
3 in 5    = 60.00%
5 in 8    = 62.50%
2 in 3    = 66.67%
7 in 10   = 70.00%
5 in 7    = 71.43%
3 in 4    = 75.00%
7 in 9    = 77.78%
4 in 5    = 80.00%
5 in 6    = 83.33%
6 in 7    = 85.71%
7 in 8    = 87.50%
8 in 9    = 88.89%
9 in 10   = 90.00%
11 in 12  = 91.67%
14 in 15  = 93.33%
19 in 20  = 95.00%
29 in 30  = 96.67%
39 in 40  = 97.50%
49 in 50  = 98.00%
59 in 60  = 98.33%
69 in 70  = 98.57%
79 in 80  = 98.75%
89 in 90  = 98.89%
99 in 100 = 99.00%
>99 in 100

Other stipulations:

• Numeric input can be in the range of 0.1 to 99.9 or in the range of 0.001 to 0.999, whichever is more convenient. You must handle at least 3 significant digits.
• You must output a ratio ("3 in 4"), not the equivalent fraction ("3/4").
• If there are two ratios equally close to the input, your program can return either one. 7.5% could return "1 in 12" or "1 in 15".
• Leading/trailing white space and/or new lines are fine

Examples:

Input  :   Output
0.5   :  <1 in 100
1.0   :   1 in 100
1.5   :   1 in 70
7.5   :   1 in 15  or  1 in 12 (either is acceptable)
9.2   :   1 in 10
13.1   :   1 in 8
29.2   :   2 in 7
29.3   :   3 in 10
52.7   :   1 in 2
52.8   :   5 in 9
72.0   :   5 in 7
73.9   :   3 in 4
88.8   :   8 in 9
90.8   :   9 in 10
94.2   :  19 in 20
98.7   :  79 in 80
98.9   :  89 in 90
99.0   :  99 in 100
99.1   : >99 in 100

This is a challenge, shortest code in each language wins.

(Similar to, but not duplicate of: Convert a decimal to a fraction, Closest fraction, Approximate floating point number with n-digit precision)

• If there are two ratios equally close to the input, your program can return either one. 7.5% could return "1 in 12" or "1 in 15" Does that mean that we can return 7 in 100 as well? Btw, 1 in 14 is closer to the input in this case. – DimChtz Aug 29 '18 at 14:48
• @DimChtz No, as that violates rule 3 (values under 10% should be expressed as "1 in n", for specific possible values of n). – BradC Aug 29 '18 at 14:50
• Ohh, I didn't notice this. Okay. – DimChtz Aug 29 '18 at 14:51
• I'd like it if we could just output the numerator and denominator as any format like a tuple/list or something, but there are already competing answers so I suppose it's too late for this challenge. For future challenges though, I'd consider a more flexible I/O format because some languages lose more competitiveness than others when you require string handling. – HyperNeutrino Aug 29 '18 at 21:12
• @BradC - LOL. I was just at 538, and I was all "Wow! I gotta make a golfing challenge out of this!" – Chas Brown Aug 29 '18 at 21:23

T-SQL, 385 bytes

SELECT TOP 1IIF(i>.99,'>',IIF(i<.01,'<',''))+n+' in '+d
FROM t,(SELECT ISNULL(PARSENAME(value,2),'1')n,PARSENAME(value,1)d FROM
STRING_SPLIT('100,90,80,70,60,50,40,30,20,15,12,10,9,8,7,6,5,2.9,4,2.7,3.10,3,3.8,2.5,3.7,4.9,2,5.9,4.7,3.5,5.8,2.3,7.10,5.7,3.4,7.9,4.5,5.6,6.7,7.8,8.9,9.10,11.12,14.15,19.20,29.30,39.40,49.50,59.60,69.70,79.80,89.90,99.100',','))m
ORDER BY ABS(i-ABS(n)/d)

Input is via pre-existing table t with numeric field i, per our IO standards.

That input table is joined with an in-memory table parsed from a string via STRING_SPLIT (which separates rows) and PARSENAME (which separates numerator and denominator via .).

The table is sorted by distance from the input value i, and returns the top row, formatted appropriately.

Charcoal, 84 bytes

ＮθＦ¹¹«Ｆ⊖ι⊞υ⟦⊕κι⟧≔⎇⊖ι∨×χι¹²¦¹⁵ιＦ²⊞υ⟦∨κ⊖ιι⟧»≔Ｅυ↔⁻θ∕§ι⁰§ι¹η≔⌕η⌊ηη×<‹θ·⁰¹×>›θ·⁹⁹⪫§υη in

Try it online! Link is to verbose version of code. Takes input as a decimal rather than a percentage. Explanation:

Ｎθ

Input the fraction.

Ｆ¹¹«

Run from $n = 0$ to $n = 10$.

Ｆ⊖ι⊞υ⟦⊕κι⟧

Generate ratios for $\frac 1 n$ to $\frac {n - 1} n$.

≔⎇⊖ι∨×χι¹²¦¹⁵ι

Get the $n^{th}$ element of the list $12, 15, 20 ... 100$ into $n$.

Ｆ²⊞υ⟦∨κ⊖ιι⟧»

Generate ratios for $\frac {n - 1} n$ and $\frac 1 n$.

≔Ｅυ↔⁻θ∕§ι⁰§ι¹η

Calculate the decimal values of all the ratios and take the absolute difference with the original input.

≔⌕η⌊ηη

Find the index of the least absolute difference. In case of a tie (e.g. $\frac 1 2$ and $\frac 2 4$), take the ratio generated first.

×<‹θ·⁰¹

Print a < if the input is less than $0.01$.

×>›θ·⁹⁹

Print a > if the input is greater than $0.99$.

⪫§υη in

Join the numerator and denominator of the appropriate ratio with in and print.

JavaScript (ES7), 164159 144 bytes

Expects an input ratio in $]0,1[$.

r=>(g=m=>--n+11?g((q=n>1?n*10:n+10-~'13'[n],d=((p=r<.1?1:r>.9?q-1:n<0&&r*q+.5|0)/q-r)**2)>m?m:(o=p+' in '+q,d)):r<.01?'<'+o:r>.99?'>'+o:o)(n=11)

Try it online!

How?

We try all possible ratios $p/q$. For each of them, we compute:

$$d=(p/q-r)^2$$

We update the best score $m$ (the lower, the better) each time $d$ is lower than or equal to $m$.

We go from the highest value of $q$ to the lowest one, so that a smaller denominator is preferred in case of a tie.

Commented

r => (g = m =>               // r = input; g() = recursive function, taking m = best score
--n + 11 ?                 // decrement n; if n is still greater than or equal to -10:
g(                       //   do a recursive call to g():
( q =                  //     compute q = denominator:
n > 1 ?              //       if n is greater than 1:
n * 10             //         q = n * 10 (20, 30, ..., 100)
:                    //       else:
n + 10 - ~'13'[n], //         q = 12 if n = 0, 15 if n = 1, n + 11 if n < 0
d = ((               //     compute d = (p / q - r)²:
p =                //       compute p = numerator:
r < .1 ?           //         if r is less than 0.01:
1                //           p = 1
:                  //         else:
r > .9 ?         //           if r is greater than 0.90:
q - 1          //             p = q - 1
:                //           else:
n < 0 &&       //             if n is negative (i.e. q is in [1,10]):
r * q + .5 | 0 //               p = round(r * q)
//             otherwise: p = 0 (which will be ignored)
) / q - r          //       compute p / q - r
) ** 2               //       and square the result (cheaper than absolute value)
) > m ?                //     if d is greater than m:
m                    //       leave m unchanged
: (                    //     else:
o = p + ' in ' + q,  //       update the output string o
d                    //       and update m to d
))                       //   end of recursive call
:                          // else (all possible ratios have been tried out):
r < .01 ? '<' + o :      //   if r is less than 0.01, prefix with '<'
r > .99 ? '>' + o :      //   if r is greater than 0.99, prefix with '>'
o                        //   otherwise, just return o
)(n = 11)                    // initial call to g() with m = n = 11

Jelly, 58 bytes

⁵R×⁵;12,15µ’,1,€)Ẏ;⁵Œc¤ð÷/ạ¥ÞḢj“ in ”
”<”>“”>.99$?<.01$?;Ç

Try it online!

-16 bytes thanks to Arnauld (can just prepend the < and > instead of rewriting the whole phrase)
-6 bytes and bug-fixes thanks to Jonathan Allan

• @Arnauld Oh you're right, never thought about that :P Thanks! – HyperNeutrino Aug 30 '18 at 0:18
• 0.3 should result in 3 in 10 not 2 in 7 – Jonathan Allan Aug 30 '18 at 7:45
• You should just remove the µµ, no? EDIT - and then golf ÐṂṂ to ÞḢ – Jonathan Allan Aug 30 '18 at 7:45
• changing 9 to should resolve the bug I believe. – Jonathan Allan Aug 30 '18 at 8:09
• @JonathanAllan Oh whoops, yep I wasn't using 10 as a valid denominator. Thanks. And no, removing the double mu doesn't work because then the "minimum" is attached to the right side of the dyadic link-min function which is definitely not what I want, but only putting one mu doesn't seem to fix it. Thanks for the golf though :D – HyperNeutrino Aug 31 '18 at 15:06

Python 2, 261278261237 177 bytes

lambda n:' <>'[(n<.01)-(n>.99)]+'%d in %d'%min([(a,b)for b in[[12,15]+r(10,110,10),r(1,11)][.1<n<.9]for a in r([1,b-1][n>.9],[b,2][n<.1])],key=lambda(a,b):abs(1.*a/b-n))
r=range

Try it online!

• Doesn't Python support semicolons? You could replace '\n ' with ';'... unless I'm wrong. – Dev Aug 30 '18 at 2:12
• @BradC Fixed :) – TFeld Aug 30 '18 at 6:49

Clean, 224198 197 bytes

import StdEnv,Data.List,Text
t=toReal
$p=if(p<1.0)"<"if(p>99.0)">"""+snd(minimum[(abs(p-t n*1E2/t d),n<+" in "<+d)\\i<-[10,12,15:[20,30..100]],(n,d)<-[(1,i),(i-1,i):diag2[1..10][1..10]]|gcd n d<2]) Try it online! Explained: t = toReal // give toReal a shorter name$ p
= if(p < 1.0)                          // if the percentage is less than 1%
"<"                                   // prepend "<"
if(p > 99.0)                           // if the percentage is > 99%
">"                                   // prepend ">"
""                                    // otherwise prepend nothing
+ snd (                                // to the second element of
minimum [                             // the smallest item in a list composed of
(                                    // pairs of
abs (                               // the absolute value of
p -                                // the difference between the percentage
t n*1E2 / t d                      // and the ratio
)
,                                    // associated with
n <+ " in " <+ d                    // the string representation of the ratio
)                                    // in the form of a tuple
\\ i <- [10, 12, 15: [20, 30..100]]  // for every special denominator i
, (n, d) <- [(1, i), (i - 1, i): diag2 [1..10] [1..10]]
// for every ratio n : d
| gcd n d < 2                        // where n / d cannot be further simplified
]
)

Jelly,  53  52 bytes

_.01,.99Ṡµ<0ịØ<ḣE⁵Ż×⁵+12,5Ṡ,’Ɗż€$Ẏ;⁵Œc¤÷/ạ¥Þ³Ḣj“ in A full program which prints the result. Try it online! Or see the test-suite Note that the test-suite is altered to make the code a monadic link by: 1. using the register keep a track of the current "program input", with ³ to ®; and 2. closing the list of characters code for " in ", with “ in to “ in ” How? Starts with code that forces any necessary printing of the < or > sign and then code which constructs all the numerator-denominator pairs (with some redundant not simplified form versions, all after their simplified form) and prints the minimally different division-evaluated entry using a stable sort joined with in . _.01,.99Ṡµ<0ịØ<ḣE⁵Ż×⁵+12,5Ṡ,’Ɗż€$Ẏ;⁵Œc¤÷/ạ¥Þ³Ḣj“ in  - Main Link: number in [0,1], n
.01,.99                                             - literal pair = [0.01, 0.99]
_                                                    - subtract -> [n - 0.01, n - 0.99]
Ṡ                                            - sign (vectorises) (-1 if <0; 1 if >0; else 0)
µ                                           - start a new monadic link
-   call that X
<0                                         - less than zero? (vectorises)
Ø<                                      - literal list of characters = "<>"
ị                                        - index into (vectorises) ("<<" if n < 0.01; ">>" if n >= 0.99; else "><")
E                                    - all (of X) equal? (1 if ((n < 0.01) OR (n > 0.99)) else 0
ḣ                                     - head to index ("<" if n < 0.01; ">" if n > 0.99; else "")
-   (the following nilad forces a print of that)
⁵                                   - literal 10
Ż                                  - zero-range -> [0,1,2,3,4,5,6,7,8,9,10]
×⁵                                - multiply by 10 -> [0,10,20,30,40,50,60,70,80,90,100]
12,5                           - literal pair = [12,5]
+                               - add -> [12,15,20,30,40,50,60,70,80,90,100]

Try it online!