How Many Guineas in a Gross of Threepennies?

32

8

Until decimalization in 1971, British money was based on dividing the pound into 240 pennies. A shilling was 12 pennies so 20 shillings made a pound. The smallest denomination was the farthing at one fourth of a penny. There were many other denominations and nicknames for coins, which can get quite confusing if you're not used to the system.

Challenge

Write a program or function that can convert (almost) any denomination of old English money to any other. To make it easier for the user you need to support plurals and nicknames.

These are the denominations and their synonymous terms you must support. For convenience their value in farthings leads each line.

1: farthing, farthings
2: halfpence, halfpenny, halfpennies
4: penny, pennies, pence, copper, coppers
8: twopenny, twopennies, twopence, tuppence, half groat, half groats
12: threepence, threepenny, threepennies, threepenny bit, threepenny bits, thruppence, thrupenny, thrupennies, thrupenny bit, thrupenny bits
16: groat, groats
24: sixpence, sixpenny, sixpennies, sixpenny bit, sixpenny bits, tanner, tanners
48: shilling, shillings, bob
96: florin, florins, two bob bit, two bob bits
120: half crown, half crowns
240: crown, crowns
480: half sovereign, half sovereigns
504: half guinea, half guineas
960: pound, pounds, pounds sterling, sovereign, sovereigns, quid, quids
1008: guinea, guineas

(I am not British, this list is by no means authoritative but it will suffice for the challenge.)

Via stdin or function argument you should take in a string of the form

[value to convert] [denomination 1] in [denomination 2]

and return or print

[value to convert] [denomination 1] is [converted value] [denomination 2]

where [converted value] is [value to convert] units of denomination 1 converted to denomination 2.

The [value to convert] and [converted value] are positive floats. In the output both should be rounded or truncated to 4 decimal places. If desired you may assume [value to convert] always has a decimal point and zero when input (e.g. 1.0 instead of 1).

Denominations 1 and 2 may be any two terms from the list above. Don't worry about whether they're plural or not, treat all denominations and synonyms the same. You may assume the input format and denominations are always valid.

Examples

1 pounds in shilling1 pounds is 20 shilling
(1.0000 pounds is 20.0000 shilling would be fine)

0.6 tuppence in tanner0.6 tuppence is 0.2 tanner

24 two bob bits in pounds sterling24 two bob bits is 2.4 pounds sterling

144 threepennies in guineas144 threepennies is 1.7143 guineas

Scoring

The shortest code in bytes wins.

Calvin's Hobbies

Posted 2014-11-03T15:25:54.897

Reputation: 84 000

1"pennies" is only ever used to refer to a number of coins, not an amount of money. – David Richerby – 2014-11-03T19:45:54.877

4

Nit-pick: Post decimalisation, the plural of quid is quid. Most likely this would have been the same with the old money. Example: Five quid a pint! Cor blimey guvnor. Exception: quids-in

– Digital Trauma – 2014-11-03T21:30:44.230

7I would probably mess alot of people up to require them to include "ha'penny". – kaine – 2014-11-03T21:51:18.487

3

I've never heard a ha'penny called anything but a ha'penny, @kaine. As in http://en.wikipedia.org/wiki/Ha%27penny_Bridge. Of course, I'm too young meself to have heard it too often in speech, but the apostrophe seems standard in writing.

– TRiG – 2014-11-03T23:35:28.113

Answers

9

Pyth, 146 145

K4J24L?*y>b-5>b\t?2>b\t.5}<b2"hatw"@[1K8K12K16J48J1008*JT96*2J960)xc"fapetucothengrsishtagucrflbo"2<b2AGHcz" in"++G%" is %0.4f"*vhcGdcyjdtcGdytHH

More readable (newlines and indents must be removed to run):

K4J24
L?*y>b-5>b\t?2>b\t.5
  }<b2"hatw"
  @[1K8K12K16J48J1008*JT96*2J960)
   xc"fapetucothengrsishtagucrflbo"2<b2
AGHcz" in"
++G
  %" is %0.4f"
   *vhcGdcyjdtcGdytH
 H

Update: Turns out, it's 1 character shorter (no space needed) to chop up the string into a list of 2 character strings before running the string index operation. /x"string"<b2 2 -> xc"string"2<b2. Nothing else needs to be changed.

How it works:

  • This uses @xnor's approach of looking up the value of currency using its first two letters, as well as the trick of detecting the initial half or two, removing it and calling the function again.

  • To lookup the value of the first two characters, it finds the location of the first two letters of the currency in a string, then divides by 2 and takes the value at that index in the list. This is much shorter than an dict in pyth.

  • Uses the fact that x (find within string) returns -1 on failure to avoid putting po (pounds) qu (quid) or so (sovereigns) in the string, and simply return the last element of the list, 960, by default.

  • By rearranging the order of currencies in the lookup system and initializing carefully, with K4 and J24, all spaces that would have been needed to separate numbers in the list were removed.

  • Uses pyth's dual assignment operator, A, on the input split on in to get the beginning and end of the input in separate variables.

  • Does essentially the same lookup at the end, though pyth has no .split(_,1), so it is somewhat more cumbersome.

Examples:

$ pyth programs/currency.pyth <<< '5 florins in half guineas'
5 florins is 0.9524 half guineas

$ pyth programs/currency.pyth <<< '0.4 quid in sixpenny bits'
0.4 quid is 16.0000 sixpenny bits

isaacg

Posted 2014-11-03T15:25:54.897

Reputation: 39 268

3I give up... ;) – Martin Ender – 2014-11-04T11:48:21.620

I didn't know < and > worked as string/list slice operators; that's much, much better than taking the head or end of a chop :) – FryAmTheEggman – 2014-11-04T14:53:37.133

@FryAmTheEggman Looks like that was missing from the documentation as well - I've added it. – isaacg – 2014-11-04T15:07:54.460

I should probably read through macros.py more carefully :) – FryAmTheEggman – 2014-11-04T15:14:07.883

14

Ruby, 345 306 302 288 287 278 273 253 252 242 232 221 202 190 bytes

f=->s{" !#+/7OďǿȗϟЏ'"[%w{fa fp ^pe|co r..p ^gr x|ta sh|^b fl|b f.c ^c f.s .gu d|v ^g .}.index{|k|s[/#{k}/]}].ord-31}
$><<gets.sub(/ (.+ i)n /){" #{r=$1}s %0.4f ".%$`.to_f/f[$']*f[r]}

Takes input from STDIN and prints to STDOUT.

I'm using short regular expressions to match only the desired denominations for each value. There are two arrays, one with regexes and one with values, at corresponding indices. The regex array is a space delimited array literal, and the value array is packed into a string of UTF-8 characters.

I'm selecting the index into the values by searching for a regex that matches each denomination. I'm also defaulting to the tuppence/half-groat case (value 8), because that required the longest regex. Likewise, some of the patterns assume that other values have already been matched by earlier patterns, so each regex only distinguishes the desired value only from the remaining ones. Using this, I could probably shave off another couple of bytes by rearranging the order of the denominations.

Thanks to Ventero for helping me beat Pyth making it shorter!

Martin Ender

Posted 2014-11-03T15:25:54.897

Reputation: 184 808

1It's the regex matching (s[k]) that overwrites $1 etc. You can save a few characters by moving the map block into a lambda and calling that directly in the last line (which also allows you to drop the assignments for $1 and $2). Also .index is shorter than .find_index. – Ventero – 2014-11-03T19:03:36.730

@Ventero Ah, that makes sense. Thank you! – Martin Ender – 2014-11-03T19:08:20.717

1Regexp.new k/#{k}/ and $><<gets.sub(/foo/){a=$3;...}gets[/foo/];a=$3;puts... for a total of 221. And you can of course use the old trick of packing the int array into a string (using .pack("U*")) and then indexing into that string. Should get you down to 195 chars/200 bytes. – Ventero – 2014-11-04T00:16:11.443

Even better: a=gets[/foo/,3] – Ventero – 2014-11-04T00:24:06.193

@Ventero Thanks a lot. I ended up with 196/202, because I added an offset to the char codes to avoid unprintable ASCII. Still shorter than Pyth. ;) – Martin Ender – 2014-11-04T09:38:15.947

Turns out .sub is actually shorter when utilizing the fact that we don't have to include the full input string in the match: $><<gets.sub(/ (.+ i)n /){" #{r=$1}s %0.4f ".%$`.to_f/f[$']*f[r]} (This now also uses %0.4f instead of .round 4, as most other answers do) – Ventero – 2014-11-04T13:29:58.190

@Ventero Ah, I keep forgetting about $\`` and$'`. Will test and update later. – Martin Ender – 2014-11-04T14:05:32.527

8

Python 3: 264 239 chars

f=lambda c:c[:2]in"hatw"and f(c[5-(c>'t'):])*2/4**(c<'t')or[1,4,4,4,8,12,16,24,24,48,48,96,240,1008,960]['fapecoentuthgrsitashboflcrgu'.find(c[:2])//2]
a,b=input().split(" in ")
x,c=a.split(" ",1)
print(a,"is %0.4f"%(eval(x)*f(c)/f(b)),b)

The function f gets the shilling value of the currency-string c by fingerprinting the first two letters using the dictionary by finding them in a string. The prefixes "half" and "two" are detected and accounted for by chopping the prefix and the space and applying a multiplier. Since "halfpenny" lacks a space after "half", this results in "enny", but that's handled with a fictional "en" entry.

Thanks to @isaacg and @grc for lots of improvement on the dictionary lookup.

xnor

Posted 2014-11-03T15:25:54.897

Reputation: 115 687

I knew it could be done :) I'm also extremely embarrassed that I didn't know you could define a dictionary like that... :S – FryAmTheEggman – 2014-11-03T19:13:32.510

2@FryAmTheEggman I didn't you could define dictionaries via keywords either until I saw it used in an answer on this site. The things you learn golfing... – xnor – 2014-11-03T19:15:01.327

I made a Pyth version of this and got 207 chars. Would you rather I post it here for you to add, or post a community wiki answer? – FryAmTheEggman – 2014-11-03T20:24:31.377

1+1 for that 2/4**(c<'t') part. – njzk2 – 2014-11-03T21:56:22.457

1You can save 13 characters by using .get(c[:2],960) to lookup the value from the dictionary and omitting the po=960,so=960,qu=960, entries from the dictionary. – isaacg – 2014-11-04T09:43:57.790

This is a little shorter again: [1,4,4,4,8,12,16,24,24,48,48,96,240,1008,960]['fapecoentuthgrsitashboflcrgu'.find(c[:2])//2] – grc – 2014-11-04T11:19:53.980

@xnor You appear to be missing the paratheses after input. – isaacg – 2014-11-05T23:58:38.887

@isaacg Thanks, I had forgotten to change that back after making it a variable for testing. – xnor – 2014-11-06T00:06:15.040

5

Python 2 - 345 358

s=str.startswith
h='half'
u,v=raw_input().split(' in ')
a,b=u.split(' ',1)
C=dict(fa=1,pe=4,twop=8,tu=8,thr=12,gr=16,si=24,ta=24,sh=48,b=48,fl=96,c=240,po=960,so=960,q=960,gu=1008)
C.update({h+'p':2,h+' gr':8,'two ':96,h+' c':120,h+' s':480,h+' gu':504})
for c in iter(C):
 if s(b,c):k=C[c]
 if s(v,c):f=C[c]
print u+' is %0.4f '%(eval(a)*k/f)+v

Requires input number to be a float in python i.e. 144.1

I think this could be shortened in python 3...

...Confirmed thanks to @xnor. Also confirmed that having a better algorithm matters a lot ;)

FryAmTheEggman

Posted 2014-11-03T15:25:54.897

Reputation: 16 206

I would replace q=raw_input().split(' in ') by q,b=raw_input().split(' in ') – njzk2 – 2014-11-03T20:32:18.050

@njzk2 Quite right... I've also used this for the next line, now :) – FryAmTheEggman – 2014-11-03T20:39:12.043

I think there is a conflict between h+' gr':8 and h+' g':504 depending on who is evaluated first for half groats – njzk2 – 2014-11-03T21:10:48.160

@njzk2 that's true... added u to the guinea one... – FryAmTheEggman – 2014-11-03T21:13:34.370

2

Haskell - 315 bytes

w x=f(u x)*v(u x)
f=maybe 1 id.l"ha tw tu th si"[0.5,2,2,3,6]
v x@(_:xs)|Just w<-l"bo cr gr gu so co fa fl pe po qu sh ta"[12,60,4,252,240,1,0.25,24,1,240,240,12,6]x=w|True=v xs
l k v x=take 2 x`lookup`zip(words k)v
u=unwords
i s|(n:x,_:t)<-span(/="in")$words s=u$n:x++["is",show$read n*w x/w t]++t
main=interact i

Rhymoid

Posted 2014-11-03T15:25:54.897

Reputation: 181

2

JavaScript (ES5), 344

I=prompt()
n=I.match(/[\d.]+ /)[0]
A=I.slice(n.length).split(" in ")
function m(x){return{fi:1,he:2,p:4,pe:4,cr:4,tn:8,hg:8,tp:12,te:12,g:16,gs:16,sn:24,tr:24,si:48,b:48,fn:96,to:96,hc:120,c:240,cs:240,hs:480,hgtrue:504,ps:960,se:960,q:960,ga:1008}[x[0]+(x[5]||"")+(x[10]=="a"||"")]}
alert(n+A[0]+" is "+(n*m(A[0])/m(A[1])).toFixed(4)+" "+A[1])

I went with a hash function approach... I think I underestimated (relatively) how complex the input processing would be (over the regex approach, that wouldn't care about the number).

FireFly

Posted 2014-11-03T15:25:54.897

Reputation: 7 107

1

Based on @FryAmTheEggMan's answer, with a different way of testing str.startwith:

Python 2: 317

h='half'
C=dict(fa=1,pe=4,twop=8,tu=8,thr=12,gr=16,si=24,ta=24,sh=48,b=48,fl=96,c=240,po=960,so=960,q=960,gu=1008)
C.update({h+'p':2,h+' gr':8,'two ':96,h+' c':120,h+' s':480,h+' gu':504})
u,v=raw_input().split(' in ')
a,b=u.split(' ',1)
s=lambda x:x and C.get(x, s(x[:-1]))
print u+' is %0.4f '%(eval(a)*s(b)/s(v))+v

njzk2

Posted 2014-11-03T15:25:54.897

Reputation: 121

I think you need to add a trailing space to the print and the formatted string. You can also rewrite the lambda as s=lambda x:x and C.get(x,s(x[:-1]))or 0 to save a character (along with the spaces). This is a pretty neat idea, btw :) – FryAmTheEggman – 2014-11-03T21:27:52.223

thanks, I fiddled for a while with this ternary notation, which I always find verbose, but did not though of the and/or thing. – njzk2 – 2014-11-03T21:34:59.143

Yeah, I learned it here :) I also think you need to make u.split(' ') say u.split(' ',1) for currencies that have spaces, like "half sovereign". – FryAmTheEggman – 2014-11-03T21:37:21.590

so that's the reason for the , 1! – njzk2 – 2014-11-03T21:38:08.613

2The ternary x and y or 0 can be shortened in general to x and y, since both evaluate to 0 or equivalently False when x is Falsey. – xnor – 2014-11-04T04:42:39.807

1

JavaScript ES6, 264 273

f=t=>{s=t.split(x=' in')
c=d=>{'t0sh|bo0^p|co0f0fp0fl|b b0gu0d|v0wn0gr0f g|t..?p0f s0f gu0f c0x|an'.split(0).map((e,i)=>{v=s[d].match(e)?[12,48,4,1,2,96,1008,960,240,16,8,480,504,120,24][i]:v})
return v}
return s.join(' is '+~~(1e4*t.split(' ')[0]*c(0)/c(1))/1e4)}

t=prompt()
s=t.split(x=' in')
c=function(d){'t0sh|bo0^p|co0f0fp0fl|b b0gu0d|v0wn0gr0f g|t..?p0f s0f gu0f c0x|an'.split(0).map(function(e,i){v=s[d].match(e)?[12,48,4,1,2,96,1008,960,240,16,8,480,504,120,24][i]:v})
return v}
alert(s.join(' is '+~~(1e4*t.split(' ')[0]*c(0)/c(1))/1e4))

This gets the value of each currency by checking it against various regexes, starting with the broadest /t/; the value is overwritten if another match is encountered. There might be a way to shave off a couple bytes by reordering the regex string. You can test it using the snippet above (It is formatted only to use dialog boxes and remove ES6 arrow functions so everyone can test the code easily). Thanks to Alconja for the suggestions.

NinjaBearMonkey

Posted 2014-11-03T15:25:54.897

Reputation: 9 925

1You can trim 2 chars by using 't0sh|bo0^p....'.split(0), 4 more by using .map instead of .forEach and 3 more by calling c(0) and c(1) and doing s[d].match – Alconja – 2014-11-04T06:17:46.787