Convert a percentage to a "simple" ratio

16

3

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)

BradC

Posted 2018-08-29T14:34:21.180

Reputation: 6 099

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 – 2018-08-29T14:48:02.780

@DimChtz No, as that violates rule 3 (values under 10% should be expressed as "1 in n", for specific possible values of n). – BradC – 2018-08-29T14:50:05.870

Ohh, I didn't notice this. Okay. – DimChtz – 2018-08-29T14:51:59.690

2I'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 – 2018-08-29T21:12:05.047

1@BradC - LOL. I was just at 538, and I was all "Wow! I gotta make a golfing challenge out of this!" – Chas Brown – 2018-08-29T21:23:55.000

Answers

6

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.

BradC

Posted 2018-08-29T14:34:21.180

Reputation: 6 099

5

JavaScript (ES7), 164 159 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

Arnauld

Posted 2018-08-29T14:34:21.180

Reputation: 111 334

5

Charcoal, 84 bytes

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

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

Nθ

Input the fraction.

F¹¹«

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

F⊖ι⊞υ⟦⊕κι⟧

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 \$.

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

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

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

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.

Neil

Posted 2018-08-29T14:34:21.180

Reputation: 95 035

4

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

HyperNeutrino

Posted 2018-08-29T14:34:21.180

Reputation: 26 575

@Arnauld Oh you're right, never thought about that :P Thanks! – HyperNeutrino – 2018-08-30T00:18:34.977

0.3 should result in 3 in 10 not 2 in 7 – Jonathan Allan – 2018-08-30T07:45:00.990

You should just remove the µµ, no? EDIT - and then golf ÐṂṂ to ÞḢ – Jonathan Allan – 2018-08-30T07:45:53.743

changing 9 to should resolve the bug I believe. – Jonathan Allan – 2018-08-30T08:09:37.387

@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 – 2018-08-31T15:06:10.397

Removing the double mu from the previous seems to work fine ...although I noticed another bug - namely 0.999 results in >1 in 1 - which can be fixed by replacing ⁵R¤p¤with⁵Œc¤` this also saves another two bytes :)

– Jonathan Allan – 2018-08-31T15:57:56.323

µ€ -> ) saves another – Jonathan Allan – 2018-08-31T18:43:12.120

@JonathanAllan Oh cool, thanks! Is the mu-euro => ) a new thing? – HyperNeutrino – 2018-09-01T06:28:35.250

Fairly new, been there since May. You left in a redundant ¤ by the way. – Jonathan Allan – 2018-09-01T08:58:56.253

@JonathanAllan Ah ok, that's about when I last used Jelly :( :P and oh yeah, thanks, I was wondering where the third byte you saved went lol. – HyperNeutrino – 2018-09-02T02:52:49.013

3

Python 2, 261 278 261 237 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!

TFeld

Posted 2018-08-29T14:34:21.180

Reputation: 19 246

1Doesn't Python support semicolons? You could replace '\n ' with ';'... unless I'm wrong. – Dev – 2018-08-30T02:12:23.490

@BradC Fixed :) – TFeld – 2018-08-30T06:49:15.863

3

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]
                                $                    - last two links as a monad
                             Ɗ                       -   last three links as a monad
                          Ṡ                          -     sign -> [1,1,1,1,1,1,1,1,1,1,1]
                            ’                        -     decrement -> [11,14,19,29,39,49,59,69,79,89,99]
                           ,                         -     pair -> [[1,1,1,1,1,1,1,1,1,1,1],[11,14,19,29,39,49,59,69,79,89,99]]
                              ż€                     -   zip with for €ach -> [[[1,12],[1,15],[1,20],[1,30],[1,40],[1,50],[1,60],[1,70],[1,80],[1,90],[1,100]],[[11,12],[14,15],[19,20],[29,30],[39,40],[49,50],[59,60],[69,70],[79,80],[89,90],[99,100]]]
                                 Ẏ                   - tighten -> [[1,12],[1,15],[1,20],[1,30],[1,40],[1,50],[1,60],[1,70],[1,80],[1,90],[1,100],[11,12],[14,15],[19,20],[29,30],[39,40],[49,50],[59,60],[69,70],[79,80],[89,90],[99,100]]
                                      ¤              - nilad followed by link(s) as a nilad:
                                   ⁵                 -   literal 10
                                    Œc               -   unordered pairs -> [[1,2],[1,3],[1,4],[1,5],[1,6],[1,7],[1,8],[1,9],[1,10],[2,3],[2,4],[2,5],[2,6],[2,7],[2,8],[2,9],[2,10],[3,4],[3,5],[3,6],[3,7],[3,8],[3,9],[3,10],[4,5],[4,6],[4,7],[4,8],[4,9],[4,10],[5,6],[5,7],[5,8],[5,9],[5,10],[6,7],[6,8],[6,9],[6,10],[7,8],[7,9],[7,10],[8,9],[8,10],[9,10]]
                                  ;                  - concatenate -> [[1,12],[1,15],[1,20],[1,30],[1,40],[1,50],[1,60],[1,70],[1,80],[1,90],[1,100],[11,12],[14,15],[19,20],[29,30],[39,40],[49,50],[59,60],[69,70],[79,80],[89,90],[99,100],[1,2],[1,3],[1,4],[1,5],[1,6],[1,7],[1,8],[1,9],[1,10],[2,3],[2,4],[2,5],[2,6],[2,7],[2,8],[2,9],[2,10],[3,4],[3,5],[3,6],[3,7],[3,8],[3,9],[3,10],[4,5],[4,6],[4,7],[4,8],[4,9],[4,10],[5,6],[5,7],[5,8],[5,9],[5,10],[6,7],[6,8],[6,9],[6,10],[7,8],[7,9],[7,10],[8,9],[8,10],[9,10]]
                                           Þ         - sort by:
                                          ¥          -   last two links as a dyad:
                                                     -       ...(with right argument of
                                            ³        -           the program input, n)
                                        /            -     reduce by:
                                       ÷             -       division
                                         ạ           -     absolute difference
                                             Ḣ       - head
                                               “ in  - literal list of characters " in "
                                              ;      - concatenate
                                                     - implicit print

Jonathan Allan

Posted 2018-08-29T14:34:21.180

Reputation: 67 804

3

Clean, 224 198 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
  ]
 )

Οurous

Posted 2018-08-29T14:34:21.180

Reputation: 7 916

2

Perl 6, 118 bytes

{'<'x(.01>$_)~'>'x($_>.99)~(|(1..9 X ^11),|map({|(1,$_-1 X$_)},12,15,|(^11 X*10))).min({abs $_-[/] @^a}).join(' in ')}

Try it online!

nwellnhof

Posted 2018-08-29T14:34:21.180

Reputation: 10 037