14
\$\begingroup\$

Objective

Given a string with single Unicode vulgar fraction, parse it to a rational number.

Valid inputs

A valid input is one of:

  • ¼ U+00BC; one quarter

  • ½ U+00BD; one half

  • ¾ U+00BE; three quarters

  • U+2150; one seventh

  • U+2151; one ninth

  • U+2152; one tenth

  • U+2153; one third

  • U+2154; two thirds

  • U+2155; one fifth

  • U+2156; two fifths

  • U+2157; three fifths

  • U+2158; four fifths

  • U+2159; one sixth

  • U+215A; five sixths

  • U+215B; one eighth

  • U+215C; three eighths

  • U+215D; five eighths

  • U+215E; seven eighths

  • (U+215F; fraction numerator one) followed by ASCII decimal digits (U+0030 – U+0039)

  • ASCII decimal digits followed by (U+2044; fraction slash) followed by ASCII decimal digits

There are exceptions. See below.

Invalid inputs

If the denominator is zero, the parser must fall in an erroneous state. This includes:

  • Monadic failing

  • Returning an erroneous value

  • Throwing an error

Rules

  1. Encoding of the input doesn't matter.

  2. Output type and format doesn't matter either. Though native rational number type is preferred, a pair of integers is permitted.

  3. Inputs that are neither valid nor invalid fall in don't care situation. This includes:

    • Whole numbers

    • Improper fractions

    • Reducible fractions

    • Fractions with zero numerator

    • Negative fractions

Examples

(U+215B) parses to one eighth.

⅟13 (U+215F U+0031 U+0033) parses to one thirteenth.

24⁄247 (U+0032 U+0034 U+2044 U+0032 U+0034 U+0037) parses to twenty-four 247ths.

1⁄7 (U+0031 U+2044 U+0037) parses to one seventh. Note that and ⅟7 will parse to the same.

0 (U+0030) falls in don't care situation. It's a whole number.

9⁄8 (U+0039 U+2044 U+0038) falls in don't care situation. It's an improper fraction.

4⁄8 (U+0034 U+2044 U+0038) falls in don't care situation. It's reducible to one half.

(U+2189) falls in don't care situation. Its numerator is zero.

(U+002D U+00BD) falls in don't care situation. It is negative.

1⁄0 (U+0031 U+2044 U+0030) must make the parser be in erroneous state. Its denominator is zero.

Ungolfed solution

Haskell

import Control.Monad
import Data.Ratio
import Text.ParserCombinators.ReadP as ReadP
import Text.Read
import Text.Read.Lex

fractionParser :: ReadP Rational
fractionParser = choice [
    char '¼' >> return (1 % 4),
    char '½' >> return (1 % 2),
    char '¾' >> return (3 % 4),
    char '⅐' >> return (1 % 7),
    char '⅑' >> return (1 % 9),
    char '⅒' >> return (1 % 10),
    char '⅓' >> return (1 % 3),
    char '⅔' >> return (2 % 3),
    char '⅕' >> return (1 % 5),
    char '⅖' >> return (2 % 5),
    char '⅗' >> return (3 % 5),
    char '⅘' >> return (4 % 5),
    char '⅙' >> return (1 % 6),
    char '⅚' >> return (5 % 6),
    char '⅛' >> return (1 % 8),
    char '⅜' >> return (3 % 8),
    char '⅝' >> return (5 % 8),
    char '⅞' >> return (7 % 8),
    char '⅟' >> do
        d <- readDecP
        guard (0 /= d)
        return (1 % d),
    do
        n <- readDecP
        char '⁄'
        d <- readDecP
        guard (0 /= d)
        return (n % d)
    ]
\$\endgroup\$
  • \$\begingroup\$ @l4m2 The percent symbol is not considered a fraction. So it falls in don't care situation. \$\endgroup\$ – Dannyu NDos Jun 5 at 7:01
  • 10
    \$\begingroup\$ In case it gladdens anyone's heart, I'll point out that Mathematica has a builtin, ToExpression, that converts a string to a number or other mathematical expression ... that utterly fails to understand these symbols :) \$\endgroup\$ – Greg Martin Jun 5 at 16:07
  • \$\begingroup\$ @GregMartin Finally! Years after the upgoat incident, Mathematica's builtins' invulnerability takes a hit! \$\endgroup\$ – ojdo Jun 8 at 8:32
  • \$\begingroup\$ Related fun fact: the character ༳ (U+0F33) has a Numeric_Value property of -1/2. \$\endgroup\$ – jazzpi Jun 8 at 11:45
  • 2
    \$\begingroup\$ @KrzysztofSzewczyk Seriously? Zero denominators are the only incorrect input. \$\endgroup\$ – Dannyu NDos Jun 8 at 20:29

12 Answers 12

16
\$\begingroup\$

Raku, 17 bytes

{S!\⅟!1/!.EVAL}

Try it online!

Returns a rational number type. Raku actually natively supports unicode literals and operators, though it doesn't do , so we need to substitute for that. Also, division by zero won't cause an error, but returns a value that causes an Exception when you try to use or print it.

| improve this answer | |
\$\endgroup\$
  • 6
    \$\begingroup\$ The right tool for the job. :) \$\endgroup\$ – Kevin Cruijssen Jun 5 at 8:05
  • \$\begingroup\$ But what about for ["0/0","¼","½","¾","⅐","⅑","⅒","⅓","⅔","⅕","⅖","⅗","⅘","⅙","⅚","⅛","⅜","⅝","⅞","⅟13","24/247","1/7","1/0","⅟7"] { say f $_ } ;) The first exception crashes it and it does not look at the other elements! ;) \$\endgroup\$ – MBaas Jun 7 at 14:38
  • \$\begingroup\$ @MBaas 0/0 is not a valid input since the numerator is 0. Even if it wasn't, division by zero is meant to be an exception \$\endgroup\$ – Jo King Jun 9 at 0:58
  • \$\begingroup\$ Sure - but is that a reason not to evaluate the others? (I think "Invalid Inputs" indicated it should be handled) ;) \$\endgroup\$ – MBaas Jun 9 at 11:35
  • \$\begingroup\$ 'According to the Perl 6 FAQ, Perl 6 was designed to mitigate "the usual suspects" that elicit the "line noise" claim from Perl 5 critics'... Yeah, right. \$\endgroup\$ – Oleg V. Volkov Jun 10 at 21:42
9
\$\begingroup\$

PHP, 80 bytes

preg_match("~(\d+)/ ?(\d+)~",iconv('','US//TRANSLIT',$argn),$m);echo$m[1]/$m[2];

Try it online!

Just letting iconv do all the work for us ^^ this time PHP is competitive :O

| improve this answer | |
\$\endgroup\$
5
\$\begingroup\$

Charcoal, 62 bytes

¿№θ⁄⪫⪪θ⁄¦/¿№θ⅟⭆θ⎇κι1/«§”)⧴≦Y�δ↥1≧Y”℅θ/I⊕§”)¶@≦IG@℅⁵∧ψ”℅θ

Try it online! Link is to verbose version of code. Outputs a pair of integers separated with / for convenience. Explanation:

¿№θ⁄

If the input contains ,

⪫⪪θ⁄¦/

then replace it with a /.

¿№θ⅟

If the input contains ,

⭆θ⎇κι1/

then replace it with 1/.

«§”)⧴≦Y�δ↥1≧Y”℅θ/I⊕§”)¶@≦IG@℅⁵∧ψ”℅θ

Otherwise look up the ordinal of the input in two compressed strings, one for the numerator, one for the denominator, and then increment the denominator (so that 1/10 can be handled).

Alternative version for 73 bytes:

¿№θ⁄«≔I⪪θ⁄θI∕§θ⁰⊟θ»¿№θ⅟«I∕¹I✂θ¹»I∕⊕§”)➙⧴ω⪪B⪪◨ιY”℅θ⊕§”)¶@≦IG@℅⁵∧ψ”℅θ

Try it online! Link is to verbose version of code. Output is a decimal fraction. Explanation:

¿№θ⁄«≔I⪪θ⁄θI∕§θ⁰⊟θ»

Handle the case by dividing the numerator by the denominator.

¿№θ⅟«I∕¹I✂θ¹»

Handle the case by taking the reciprocal of the denominator.

I∕⊕§”)➙⧴ω⪪B⪪◨ιY”℅θ⊕§”)¶@≦IG@℅⁵∧ψ”℅θ

Look up the decremented numerator and denominator in two compressed strings and divide their increments. (As I need the numerator as an integer anyway, incrementing it actually makes the string more compressible.)

| improve this answer | |
\$\endgroup\$
5
\$\begingroup\$

JavaScript (ES6),  125 120  106 bytes

s=>([n,d]=s.split(/\D/),d)?(n||1)/d:'131111121234151357'[i=s.charCodeAt()%63%20]/-~'133689224444557777'[i]

Try it online!

| improve this answer | |
\$\endgroup\$
4
\$\begingroup\$

Groovy, 96 bytes

import static java.text.Normalizer.*
def f={s->Eval.me(normalize(s,Form.NFKC).replace("⁄","/"))}

Try it online!

Uses Unicode Normalization to replace the digits with ASCII digits. Then replaces the fraction slash by an ASCII slash and evaluates the resulting string.

| improve this answer | |
\$\endgroup\$
3
\$\begingroup\$

Retina 0.8.2, 136 bytes

T`¼-¾⅐-⅟`L
B
1/2
D
1/7
E
1/9
F
1/10
G|H
$&3
A|C
$&4
[I-L]
$&5
M|N
$&6
[O-R]
$&8
[AGIMOS]
1/
H|J
2/
[CKP]
3/
L
4/
N|Q
5/
R
7/
⁄
/

Try it online! Link includes test cases. Explanation: By transliterating the UTF-8 characters to uppercase letters we can avoid massive UTF-8 encoding penalties which would otherwise make the shortest solution a series of replacements one for each fraction character (154 bytes in total). Instead I can squeeze out a few bytes by sharing replacements between fractions with the same denominator and then again with the numerator, particularly with fifths and eighths.

| improve this answer | |
\$\endgroup\$
3
\$\begingroup\$

05AB1E, 90 88 65 bytes

gi•QλÖìʒ¨ù·'á!÷€āW•2ô2ÝƵ∞+14ÝŽX=+«çIkè`>/ëćÇŽX€åizë\ŽW&ç¡`D_iõEë/

Try it online or verify all test cases.

Explanation:

gi                       # If the length of the (implicit) input is 1:
  •QλÖìʒ¨ù·'á!÷€āW•     '#  Push compressed integer 131133161819122214243444155517375777
                   2ô    #  Split it into parts of size 2:
                         #   [13,11,33,16,18,19,12,22,14,24,34,44,15,55,17,37,57,77]
  2Ý                     #  Push list [0,1,2]
    Ƶ∞                   #  Push compressed integer 188
      +                  #  Add it to each value
       14Ý               #  Push list [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14]
          ŽX=            #  Push compressed integer 8528
             +           #  Add it to each value
              «          #  Merge the two lists together
               ç         #  Convert each to a character: 
                         #   ["¼","½","¾","⅐","⅑","⅒","⅓","⅔","⅕","⅖","⅗","⅘","⅙","⅚","⅛","⅜","⅝","⅞"
                Ik       #  Get the index of the input in this list
                  è      #  Use it to index into the list we created earlier
                   `     #  Pop and push both digits separated to the stack
                    >    #  Increase the denominator by 1
                     /   #  And divide them by one another
 ë                       # Else (the length is not 1):
  ć                      #  Extract the first character of the (implicit) input
   Ç                     #  Convert it to a unicode integer-list
    ŽX€                  #  Push compressed integer 8543
       åi                #  If it's in the list (thus the first character was '⅟'):
         z               #   Push 1 divided by the remaining value
        ë                #  Else (the first character is not '⅟'):
         \               #   Discard the partial input
          ŽW&            #   Push compressed integer 8260
             ç           #   Convert it to a character "⁄"
              ¡          #   Split the (implicit) input by "⁄"
               `         #   Push both values separated to the stack
                D_i      #   If the top value (the denominator) is 0:
                   õE    #    Loop "" amount of times, resulting in an error
                  ë      #   Else:
                   /     #    Divide the two values by one another
                         # (after which the result is output implicitly)

See this 05AB1E tip of mine (section How to compress large integers?) to understand how all the compressed integers work.

| improve this answer | |
\$\endgroup\$
3
\$\begingroup\$

Python 3, 78 76 bytes

lambda s:eval(normalize('NFKC',s).replace(*'⁄/'))
from unicodedata import*

Try it online!

A lazy built-in solution in Python.

2 bytes saved by dingledooper.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ 76 bytes \$\endgroup\$ – dingledooper Jun 6 at 19:33
  • \$\begingroup\$ Does the result conform to the output requirements? Although they allow any (complex, i. e. not simple) number type, I don’t think that numerical approximations are permitted. See my candidate for an explanation of an exact solution. \$\endgroup\$ – David Foerster Jun 7 at 13:23
  • \$\begingroup\$ This was also my first impression after reading the task, but almost all other answers do the same thing, so I guess everybody is fine with that... \$\endgroup\$ – Kirill L. Jun 7 at 13:32
2
\$\begingroup\$

Python 3, 273 \$\cdots\$ 248 240 bytes

Saved 8 bytes thanks to pppery!!!

lambda s:{'¼':1/4,'½':.5,'¾':3/4,'⅐':1/7,'⅑':1/9,'⅒':.1,'⅓':1/3,'⅔':2/3,'⅕':.2,'⅖':.4,'⅗':.6,'⅘':.8,'⅙':1/6,'⅚':5/6,'⅛':1/8,'⅜':3/8,'⅝':5/8,'⅞':7/8}.get(s[0])or eval(s.replace('⁄','/').replace('⅟','1/'))

Try it online!

A straight-up mapping of Unicode vulgar fractions to their values ( (U+2189) falls into the "don't care" situation so throws an exception) or failing that, replaces (U+215F) with 1/ (ASCII 49 + ASCII 47) and (U+2044) with / (ASCII 47) and does an eval. So either returns the value (for valid input) or throws an exception (for invalid input).

| improve this answer | |
\$\endgroup\$
  • 1
    \$\begingroup\$ Is there some reason that you can't remove '↉':1 entirely, thereby causing that value to be treated as invalid and throw an exception? \$\endgroup\$ – pppery Jun 5 at 20:02
  • \$\begingroup\$ @pppery Oh yeah, of course. Nice one - thanks! :-) \$\endgroup\$ – Noodle9 Jun 5 at 20:36
2
\$\begingroup\$

Python 3, 153 151 bytes

lambda s:F(s.translate({8260:47,8543:"1/"}))if s[1:]else F(numeric(s)).limit_denominator()
from fractions import Fraction as F
from unicodedata import*

Try it online!


A not-so-lazy Python solution that returns the correct rational number as a fraction.Fraction instead of its floating point approximation.

Explanation

We need to distinguish between two cases:

Case 1: input length ≠ 1

Fraction( s.translate({ 8260: 47, 8543: "1/" }) )

Replace a couple of codepoints

  • 826010 (U+2044, ) → 4710 (U+002F, /)
  • 854310 (U+215F, ) → 1/

and parse the result as a Fraction.

Case 2: input length = 1

Fraction(unicodedata.numeric(s)).limit_denominator(10)

Convert the input codepoint into its floating point representation using unicodedata.numeric and subsequently as a Fraction. Unfortunately the intermediate representation as a floating point value loses some precision and we need to approximate the intended value using Fraction.limit_denominator and the knowledge of the largest occurring denominator 1,000,000 (actually 10 but the function’s default argument value is good enough).

Unfortunately we cannot merge the code paths for conversion to Fraction of both cases since the denominator limitation will return incorrect results for large denominators in case 1.

| improve this answer | |
\$\endgroup\$
1
\$\begingroup\$

Perl5:

perl -CiIO -Mutf8 -MUnicode::Normalize -pe '$_=NFKD$_'
| improve this answer | |
\$\endgroup\$
  • 1
    \$\begingroup\$ Could you possibly add your byte count? \$\endgroup\$ – Jonathan Frech Jun 6 at 20:51
1
\$\begingroup\$

C (gcc) with -funsigned-char, 317 239 234 bytes

Thanks to ceilingcat for the changes!

To save some size, I preload the numerator with 1 (the denominator will usually be set to 0 on test failures so that the function will return an invalid value: generally NAN or INF, but some of the "don't care" inputs give other values.) I then look for the common cases and return the result.

It's a somewhat brute-forced function, given that I'm not using any character classification or normalization to assist: I decode the first byte to determine whether I'm looking at a precomposed fraction or not, and then apply rules from what is found.

#define S(x)strtol(x,&s,0)*!
n,d;float f(char*s){n=1;d=*s<58?n=S(s)0,*s|s[1]?s+=3,S(s)*s:0:*s-194?d=0[s+=2]-144,d<1?7:d<2?9:d<3?10:d<5?n=d-2,3:d<9?n=d-4,5:d<11?n=4*d-35,6:d<15?n=2*d-21,8:S(++s)*s:*++s<191?n=*s-187,4:0;return(n+.0)/d;}

Try it online!

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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