12
\$\begingroup\$

Whyte Notation is a classification method mainly for steam locomotive, that classifies by wheel arrangement. On a steam locomotive ( we're only focusing on non articulated locomotives here ), the wheels are generally disposed like this : you have a specific number of leading wheels, then a specific number of driver wheels, and then a specific number of trailing wheels. On the top of that, each different wheel arrangement has its own name.

For example, look at this machine :

SNCF Class 141R

It has 2 leading wheels ( 1 on each sides ), 8 driving wheels ( 4 on each sides ) and 2 trailing wheels ( 1 on each sides )

Its Whyte Notation would be 2-8-2, and its name associated with it is "Mikado". Based on the Whyte Notation, we can create a visual arrangement that can be represented by a string, with the o character as a small wheel ( leading wheel if in front of the driver wheels, else trailing ), and a caps O for the driver wheels

Taking the previous locomotive as an example, its visual arrangement would be this : oOOOOo, with one small o on the front ( the leading wheels ), 4 caps O in the middle ( the driving wheels ), and one small o at the end ( the trailing wheels )

Here is the conversion table that we'll use to base the code on ( the table of entries that will have to be supported by your code ) :

    Input    |   Output
-------------+----------
oO           | Planet
oOo          | Jenny Lind
oOO          | Porter
oOOo         | Columbian
ooO          | Jervis
ooOoo        | Huntington
oooO         | Cramton
OO           | Four-coupled
OOo          | Olomana
OOoo         | Forney
ooOO         | American
ooOOo        | Atlantic
ooOOoo       | Jubilee
oOOO         | Mogul
oOOOo        | Prairie
oOOOoo       | Adriatic
ooOOO        | Ten-wheeler
ooOOOo       | Pacific
ooOOOoo      | Hudson
OOOO         | Eight-coupled
oOOOO        | Consolidation
oOOOOo       | Mikado
oOOOOoo      | Berkshire
ooOOOO       | Mastodon
ooOOOOo      | Mountain
ooOOOOoo     | Northern
oooOOOOooo   | Turbine
OOOOO        | Ten-coupled
OOOOOo       | Union
oOOOOO       | Decapod
ooOOOOO      | El Gobernador
ooOOOOOo     | Overland
oOOOOOo      | Santa Fe
oOOOOOOoo    | Texas
ooOOOOOOo    | Union Pacific
ooOOOOOOOoo  | AA20

Your task

Your task is to write 2 programs of functions.

First program / function : from arrangement to name

The first function / program will be taking as input a string that exists in the table given previously, and will output the associated name ( for example, oOOOOo will return "Mikado" ).

For the input, the only inputs you have to support are the one in the table previously given. Anything else that isn't in the table leads to undefined behavior.

For the output, the string has to be the exact name, with the exception of case, which can be whatever you want. Trailing spaces are fine too.

Examples of acceptable output : Mikado, mikado, MIKADO , mIkAdO, Ten-wheeler

Examples of not acceptable output : Mi Ka Do, Mikado, Tenwheeler, Ten_wheeler, Ten wheeler

Second program / function : the opposite

This program of function will do the opposite of the first program, meaning that when given a name, it will print / return / output the string representing the wheel arrangement of the locomotive.

As input, it will take whatever name existing in the "Output" part of the previously given table. Shall the name not exist, it leads to undefined behavior

As output, it will give the exact string that represents the wheel arrangement, no flexibility allowed. So for example, Mikado as input must return oOOOOo.

Global constraints

Both of the programs have to be able to take as input the other program's output, and func2(func1(wheel_arrangement_string)) has to be equal to wheel_arrangement_string. Same goes the other way, with func1(funct2(wheel_arrangement_name)) that has to be equal to wheel_arrangement_name.

Aside from that, standard I/O rules apply, and default loopholes are forbidden

Additionnal Rules

Programs and functions can call each others, and share calls to any additional functions or use any additional data you want, as long as they are included in the score

Scoring rules

Your score will simply be the sum of each program / function code sizes, as well as the size of any additional data / function required to make the code work. Each functions need to support all the entries listed in the table previously given.

\$\endgroup\$
4
  • \$\begingroup\$ This is largely a compression challenge. To avoid duplication of the table, I assume one program/function can call the other? \$\endgroup\$ May 29, 2022 at 17:40
  • \$\begingroup\$ @LevelRiverSt Yes, you can \$\endgroup\$ May 29, 2022 at 17:47
  • \$\begingroup\$ If both programs/functions are linked, can we just build a single program/function taking an extra parameter for the translation type? \$\endgroup\$
    – Arnauld
    May 29, 2022 at 22:36
  • \$\begingroup\$ @Arnauld I don't see any reasons to disallow it, so i give a green light for this. \$\endgroup\$ May 29, 2022 at 23:25

5 Answers 5

5
\$\begingroup\$

JavaScript (Node.js), 463 bytes

A function that translates whatever string it is given in both directions.

s=>"5Planet2Four-coupled2Jervis1Jenny lind1Porter3Olomana3Cramton2American3Columbian1Mogul5Forney3Eight-coupled5Huntington2Atlantic1Ten-wheeler7Prairie1Consolidation16Ten-coupled13Jubilee2Pacific1Mastodon13Adriatic2Mikado1Decapod31Union30Hudson2Mountain1El gobernador29Berkshire2Santa fe126Northern2Overland320Union pacific126Texas380Turbine1412Aa20".replace(/(\d+)(....\D*)/g,(_,a,b)=>(a=(t+=+a).toString(2).replace(/./g,(n,i)=>i?'oO'[n]:''))==s?b:b==s?a:'',t=0)

Try it online!

How?

Each wheel arrangement is encoded as a binary pattern with a forced leading \$1\$ followed by \$0\$'s for o's and \$1\$'s for O's.

Once the list has been sorted from lowest to highest pattern under this encoding scheme, each name is prefixed with the delta value that must be added to the previous pattern to get the new one.

 Name         | Arrangement | As binary | As decimal | Delta
--------------+-------------+-----------+------------+-------
 Planet       |       oO    |     101   |      5     |   5
 Four-coupled |       OO    |     111   |      7     |   2
 Jervis       |      ooO    |    1001   |      9     |   2
 Jenny lind   |      oOo    |    1010   |     10     |   1
 Porter       |      oOO    |    1011   |     11     |   1
 Olomana      |      OOo    |    1110   |     14     |   3
 Cramton      |     oooO    |   10001   |     17     |   3
 American     |     ooOO    |   10011   |     19     |   2
 ...          |      ...    |     ...   |    ...     |  ...

Which is stored as:

"5Planet2Four-coupled2Jervis1Jenny lind1Porter3Olomana3Cramton2American..."

JavaScript (Node.js),  566  552 bytes

Saved 14 bytes thanks to @HatsuPointerKun

I personally think that the challenge is more interesting with two independent functions (and I originally assumed it was a requirement) so here is my attempt under this constraint.

The first function outputs the name with the first letter in upper case and everything else in lower case. The second function accepts the name in the same format (but it actually doesn't care about the case).

Arrangement to name,  378  364 bytes

s=>"CramtonPorterXMountainXTurbineXEl gobernadorJervisXHudsonSanta feMikadoColumbianXXDecapodMogulBerkshireAdriaticUnionOlomanaEight-coupledXAa20ForneyOverlandAtlanticUnion pacificXFour-coupledTen-wheelerPrairieNorthernHuntingtonXConsolidationPlanetPacificTen-coupledAmericanXMastodonTexasJenny lindJubilee".match(/.[^A-Z]*/g)[s.replace(/./g,c=>c>{}?3:7)%88812%47]

Try it online!

How?

The pattern is turned into an integer by replacing o's with \$3\$'s and O's with \$7\$'s. For instance, ooOOOoo becomes \$3377733\$.

This is reduced modulo \$88812\$ and modulo \$47\$ to get the final index in the lookup table, e.g. \$(3377733 \bmod 88812)\bmod 47=10\$.

The lookup table is encoded as a single string where each entry starts with a capital letter. We use a single X for undefined entries.

"CramtonPorterXMountainXTurbineXEl gobernadorJervisXHudson..."
 ^      ^     ^^       ^^      ^^            ^     ^^
 0      1     23       45      67            8     9|
                                                    10

Hence the answer for ooOOOoo: Hudson.

Name to arrangement, 188 bytes

s=>0x4489B662749DEFD3631DBEB225C432AC03F6DBD8C78503C3FE918DA62325B6895775C9FAFB8E0E03939CD081D4537BDD91B2D228n.toString(3).split`2`[parseInt(s[7]+s,33)*362%763%61].replace(/./g,c=>"Oo"[c])

Try it online!

How?

We can't use parseInt directly on the name because, for instance, Union and Union pacific would both be parsed as Union.

Instead, we use:

parseInt(s[7] + s, 33)

Example:

"Jenny lind" ~> "iJenny lind" ~> 22045409

We apply a slightly more complicated formula to the result:

$$f(n)=((n\times 362)\bmod 763) \bmod 61$$

Example:

$$f(22045409)=2$$

The patterns are encoded in ternary, with \$0\$'s for O's, \$1\$'s for o's and \$2\$'s for the separators. This leads to a pretty huge integer which is stored as a BigInt in hexadecimal.

0x4489B66274...28n.toString(3).split`2`

gives:

[ '110001', '', '101', '0000', '10001', ... ]
                  ^
                'oOo'

Hence the answer for Jenny lind: oOo.

\$\endgroup\$
6
  • \$\begingroup\$ I updated the rules to be more clear : you can have any functions and data additional to the two functions to help for the challenge as long as you include it in the score. I didn't precise it before, as i didn't see it as a challenge where you would need to have two completely different sources \$\endgroup\$ May 29, 2022 at 18:21
  • \$\begingroup\$ Great anwser, btw. What i can't figure out is how do you manage to find these kinds of methods to complete the challenge, with the "I transform o in 7 and O in 2, and then i apply some modulos to get an index that i use in a array". How do you do that ? \$\endgroup\$ May 29, 2022 at 23:38
  • 2
    \$\begingroup\$ @HatsuPointerKun Step 1 is to figure out a couple of patterns that may work, step 2 is to run a brute-force search over them, step 3 is to either stop there or resume at step 1. :-) \$\endgroup\$
    – Arnauld
    May 29, 2022 at 23:45
  • \$\begingroup\$ So, i toyed a bit with some code to find some good modulo combination, using brute-force, and i found that you can save 4 bytes by using modulo 1151 and 60, and using this string : AdriaticXPorterBerkshireXDecapodOverlandJenny lindXXMikadoXPlanetSanta feTexasXMogulXXXConsolidationColumbianFour-coupledXUnionPrairieCramtonJubileeXXXXMastodonUnion pacificXXAmericanPacificHudsonAa20HuntingtonXXEl gobernadorXTen-wheelerForneyOlomanaMountainXAtlanticEight-coupledJervisTen-coupledXTurbineNorthern, for the arrangement to name function \$\endgroup\$ May 30, 2022 at 13:52
  • \$\begingroup\$ The lowest program i got is this one : s=>"CramtonPorterXMountainXTurbineXEl gobernadorJervisXHudsonSanta feMikadoColumbianXXDecapodMogulBerkshireAdriaticUnionOlomanaEight-coupledXAa20ForneyOverlandAtlanticUnion pacificXFour-coupledTen-wheelerPrairieNorthernHuntingtonXConsolidationPlanetPacificTen-coupledAmericanXMastodonTexasJenny lindJubilee".match(/.[^A-Z]*/g)[s.replace(/./g,c=>c>{}?3:7)%88812%47], at 364 bytes, with a 2 stage modulo operation and the aim to get the second modulo as low as possible, to save on the lookup string \$\endgroup\$ May 30, 2022 at 15:58
2
\$\begingroup\$

C (gcc), 698 671 670 bytes

  • -26 thanks to ceilingcat
  • -2 using \n instead of \012

I encoded the Whyte notation into a bit encoding xxyyyzz, with xx being the leading wheels, yyy the driving wheels and zz the trailing wheels; these are stored at the beginning of each train name to save space. f() is the name-to-type conversion and g() is the type-to-name conversion.

#define F(a,b)for(;i/a;i-=a)printf(#b);
#define G(a,b)for(i*=a;b*s;s++)i++;
char*a[]={"$Planet","%Jenny Lind","(Porter",")Columbian","DJervis","FHuntington","dCramton","\8Four-coupled","\tOlomana","\nForney","HAmerican","IAtlantic","JJubilee",",Mogul","-Prairie",".Adriatic","JTen-wheeler","KPacific","LHudson","\020Eight-coupled","0Consolidation","1Mikado","2Berkshire","PMastodon","QMountain","RNorthern","iTurbine","\024Ten-coupled","\025Union","4Decapod","TEl Gobernador","UOverland","5Santa Fe",":Texas","ZUnion Pacific","^AA20"},**b;i;f(char*s){for(b=a;strcmp(s,1+*b);b++);i=**b;F(32,o)F(4,O)F(1,o)}g(char*s){G(0,111==)G(8,79==)G(4,)for(b=a;i-**b;b++);puts(1+*b);}

Try it online!

\$\endgroup\$
1
  • \$\begingroup\$ You can save a few bytes by using #define G(x)for(i*=x*s;s++)i++; and changing the usages of G accordingly. \$\endgroup\$
    – Adalynn
    Jan 6 at 16:45
1
\$\begingroup\$

C#, 846 Bytes

using System;using System.IO;using System.IO.Compression;class P{
string[]D(){byte[]b=new byte[565];new GZipStream(new MemoryStream(Convert.FromBase64String("H4sIAAAAAAAAClWRbU7DMAyG//sUvcAkxA3K2ECIkkmMA6SNWa2l8eSmg90e52MD+ssfr53Hb9nAztuAEdgwvGAIl+aVgtNUOywRJYUMa/bL1JMNwGxUKGeaU8gMz0uIFA6RU0+ba7FTSnTDlhdZDbycPDrNGYznyQabYtauBLykLQbaCYWGsl51bVSqSENJlWzpySMmFgMdHxafQ4adWBIqDdW1Tshe5wzsMay+RkSfzqgDdqDPqyDTuznDqnxDhzHeeJNAr+EwsyenW9OBRj+Gjo7Wcc0YHlCO80iiGGWos3Nklw2pA6wmWboVGN7U3BGleJZLDPtFegqYYQr8r3d5zUe4QRh4xMGeWDlrvvHNE/e6UtHqudnyM4qaWe/Rwrs6a5tt8azA7PHb5t/5953mn1VV2bb3dz9Z/JdzNQIAAA==")),(CompressionMode)0).Read(b,0,565);return System.Text.Encoding.ASCII.GetString(b).Split('\n');}
public string A(string i){var L=D();return L[Array.FindIndex(L,s=>s==i)-1];}
public string B(string i){var L=D();return L[Array.FindIndex(L,s=>s==i)+1];}}

How?

I edited the table into a bare minimum text file where the Whyte Notation lines are followed by a line with their name.

Then I gzipped the text and encoded those bytes in base64 to make a compressed string literal to store in the code.

The original text data took up 565 bytes and the base64 string is just 444 bytes.
Making a constant literal array for these strings in C# would have taken at least 781 bytes. The function to decompress and return the array is only 622 bytes.

When either function A() or B() in the program runs, it decodes and decompresses the base64 string into an array of strings. Using some simple Array.FindIndex I can find the line matching the function input and return the line either above or below the matched index.

\$\endgroup\$
1
\$\begingroup\$

Ruby, 478 474 bytes

Latest edit: rearranged the order of names to allow for all to be in caps. Saved a few bytes by grouping constants and minor formatting.

->q{s=w=3;r=p=0;e="-WHEELER";z=46
'H`0
PDavU$J24(d:1E,_I8F9iXYZeV5*%hTy'.bytes{|i|t="UNION PACIFICCONSOLIDATIONEL GOBERNADORJENNY LINDHUNTINGTONCOLUMBIANBERKSHIRESANTA FENORTHERNMOUNTAINMASTODONOVERLANDATLANTICADRIATICAMERICANPACIFICTURBINECRAMTONPRAIRIEOLOMANAJUBILEEDECAPODJERVISPORTERFORNEYHUDSONPLANETMIKADOTEXASUNIONMOGULEIGHTAA20FOURTENTEN"[p-=w,w]
x=(y=[?o*(i/4%4)+?O*(i/16-1&7)+?o*(i%4),t+e*(1&z/=2)]).index q
x&&r=y[~x]
u=t.sum
u>s||w+=w/10*2+1
s=u
e="-COUPLED"}
r}

Try it online!

A single function which takes either kind of input and returns the correct output.

This is written to allow for further compression of the name string later.

The names of the configurations are generated in order from the shortest to the longest per the table below. A checksum is made by adding all the characters and normally this checksum is nondecreasing; when the checksum decreases the code increases w by the length of the name by 1 (or when jumping from Jenny Lind to El Gobernador by 3), thus the trigger to increase the length of the name is encoded in the string of names itself. After unpacking -wheeler or -COUPLED is added as a suffix.

We iterate through the names using a string of single characters that corresponds to the wheel configuration. This is unpacked using the string ?o*(i/4%4) + ?O*(i/16-1&7) + ?o*(i%4) which covers the possibility of the front and back wheels numbering 0 through 3. Driven wheel counts n of 1 through 6 are encoded by (n+1)*16. For 7 pairs of driven wheels, the encoding is zero, plus 10 for the idler wheels giving ASCII 10 (newline) for the only locomotive with this number of wheels (AA20.) During the decoding process, this gives n=-1 which is corrected to 7 by ANDing with -7.

["ooOOO", "TEN-WHEELER"]
["OOOOO", "TEN-COUPLED"]
["OO", "FOUR-COUPLED"]
["ooOOOOOOOoo", "AA20"]
["OOOO", "EIGHT-COUPLED"]
["oOOO", "MOGUL"]
["OOOOOo", "UNION"]
["oOOOOOOoo", "TEXAS"]
["oOOOOo", "MIKADO"]
["oO", "PLANET"]
["ooOOOoo", "HUDSON"]
["OOoo", "FORNEY"]
["oOO", "PORTER"]
["ooO", "JERVIS"]
["oOOOOO", "DECAPOD"]
["ooOOoo", "JUBILEE"]
["OOo", "OLOMANA"]
["oOOOo", "PRAIRIE"]
["oooO", "CRAMTON"]
["oooOOOOooo", "TURBINE"]
["ooOOOo", "PACIFIC"]
["ooOO", "AMERICAN"]
["oOOOoo", "ADRIATIC"]
["ooOOo", "ATLANTIC"]
["ooOOOOOo", "OVERLAND"]
["ooOOOO", "MASTODON"]
["ooOOOOo", "MOUNTAIN"]
["ooOOOOoo", "NORTHERN"]
["oOOOOOo", "SANTA FE"]
["oOOOOoo", "BERKSHIRE"]
["oOOo", "COLUMBIAN"]
["ooOoo", "HUNTINGTON"]
["oOo", "JENNY LIND"]
["ooOOOOO", "EL GOBERNADOR"]
["oOOOO", "CONSOLIDATION"]
["ooOOOOOOo", "UNION PACIFIC"]

Ruby, 499 bytes

This is the compressed version. The characters A..U are compressed in groups of 3 down to 2 characters in printable ASCII range. Q does not appear in any locomotive name and is used for other characters. After decompression Q is replaced by the other characters Y VVYX20H .

It's a bit of a fail as it's longer than the original. Also many compromises were made in the original to make this compression possible so maybe I will revise the original later.

->q{s=w=3;r=p=0;e="-WHEELER";z=46;d=""
261.times{|i|d<<('a`$9*wHx`m(`b5mHA{?`V(]pE7]k]|pd{^t1dp]x]+px!p!$9y)6$esqb[#)53fKnj|;`Dui]\\"{$a`?5B ;t^^'[i/3].ord*95+"I2._M7Du2GLm>FzF{zM=p0)M7'3-ziSJzjAt`\\,u,w7z2X2+_E:2J}#A[WSngy>8fBi`wf)>,Z{~7[2fsVrW.))"[i/3].ord-3135)/21**(i%3)%21+65}
'  Y  VVYX20H`0
PDavU$J24(d:1E,_I8F9iXYZeV5*%hTy'.bytes{|i|d=~/Q/?d.sub!(?Q,i.chr): (t=d[p-=w,w]
x=(y=[?o*(i/4%4)+?O*(i/16-1&7)+?o*(i%4),t+e*(1&z/=2)]).index q
x&&r=y[~x]
u=t.sum
u>s||w+=w/10*2+1
s=u
e="-COUPLED")}
r}

Try it online!

\$\endgroup\$
1
\$\begingroup\$

Retina, (445+262)= 707 bytes

Notation -> Name, 445 bytes

O+
$.&
oo
V
V2V
Jubilee
V2o
AtlXtic
o2o
ColumbiX
2V
Forney
V2
AmericX
o2
Porter
2o
OlomXa
2
FourY
V7V
AA20
Vo4Vo
Turbine
o6V
Texas
V6o
5o V3o
V4V
Northern
V5o
OverlXd
V3V
HudsW
o4V
Berkshire
V4o
Mountain
V5
El Gobernador
o5o
SXta Fe
o3V
Adriatic
V3o
Pacific
o4o
Mikado
V4
MastodW
5o
UniW
o5
Decapod
V1V
HuntingtW
o3o
Prairie
V3
Ten-wheeler
o4
CWsolidatiW
Vo1
CramtW
o3
Mogul
o1o
Jenny Lind
V1
Jervis
o1
PlXet
5
TenY
4
EightY
W
on
X
an
Y
-coupled

Try it online!

Approach

Replace each run of Os with its length, giving us for example oo7oo. Replace each oo with V, giving us for example V7V. Then replace that shortened notation with the appropriate name.

There wasn't a whole lot of similarity between names -- the only substrings I found to be worth extracting were -coupled, an, and on

Name -> Notation, 262 bytes

Hun.*
ZOZ
....(..?).*
$1
AA20
Z7Z
at
o3Z
ic
Z2
nt
Z2o
sh
o4Z
mb
o2o
ol
o4
to
ZoO
po
o5
ob
Z5
t-
4
ey
2Z
-c
2
on
Z3Z
y 
oOo
is
ZO
le
Z2Z
od
Z4
do
o4o
ta
Z4o
he
Z4Z
an
2o
la
Z5o
et
oO
er
o2
ri
o3o
fi
Z3o
a 
o5o
wh
Z3
co
5
s
o6Z
in
Zo4Zo
n 
Z6o
n
5o
l
o3
Z
oo
\d
*O

Try it online!

Approach

Extract 2 unique characters from each name, and use that to determine the notation. I found the 5th and (if present) 6th characters to work pretty well. Huntington clashes with Turbine, so Hun.* is replaced in advance. The shortened notation is the same as what's used for the Notation->Name program, but in reverse.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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