Minecraft Language Files Updater

11

In 1.13, Minecraft language files were switched from being a simple multi-line key=value format to JSON.

Challenge

Write a program converting from the original format returning a JSON string. Input can be taken using any standard input method, output must be json from any standard output method

The original format contains lines with key=value pairs, for example

tile.dirt.name=Dirt
advMode.nearestPlayer=Use "@p" to target nearest player

build.tooHigh=Height limit for building is %s blocks

Should be converted to one large JSON object with key=value

{
    "tile.dirt.name": "Dirt",
    "advMode.nearestPlayer": "Use \"@p\" to target nearest player",
    "build.tooHigh": "Height limit for building is %s blocks"
}

Some details

  • Any valid JSON is allowed as long as it contains only the correct key/value pairs. Trailing commas are allowed because Minecraft allows them.
  • The only things that must be escaped are quotes. (No newlines, backslashes, or other json-breaking things existed in the language file prior to 1.13)
  • Empty lines should be ignored
  • Lines contain exactly one equals

Test Cases

Input:

tile.dirt.name=Dirt
advMode.nearestPlayer=Use "@p" to target nearest player

build.tooHigh=Height limit for building is %s blocks

Output:

{
    "tile.dirt.name": "Dirt",
    "advMode.nearestPlayer": "Use \"@p\" to target nearest player",
    "build.tooHigh": "Height limit for building is %s blocks"
}

Input:

translation.test.none=Hello, world!
translation.test.complex=Prefix, %s%2$s again %s and %1$s lastly %s and also %1$s again!
translation.test.escape=%%s %%%s %%%%s %%%%%s
translation.test.invalid=hi %
translation.test.invalid2=hi %  s
translation.test.args=%s %s
translation.test.world=world

Output:

{
  "translation.test.none": "Hello, world!",
  "translation.test.complex": "Prefix, %s%2$s again %s and %1$s lastly %s and also %1$s again!",
  "translation.test.escape": "%%s %%%s %%%%s %%%%%s",
  "translation.test.invalid": "hi %",
  "translation.test.invalid2": "hi %  s",
  "translation.test.args": "%s %s",
  "translation.test.world": "world",
}

Input:

stat.mineBlock=%1$s Mined
stat.craftItem=%1$s Crafted
stat.useItem=%1$s Used
stat.breakItem=%1$s Depleted

Output:

{
    "stat.mineBlock": "%1$s Mined",
    "stat.craftItem": "%1$s Crafted",
    "stat.useItem": "%1$s Used",
    "stat.breakItem": "%1$s Depleted"
}

pfg

Posted 2018-08-09T01:33:08.223

Reputation: 735

1How does tile.dirt.name become "block.minecraft.dirt"? – Pavel – 2018-08-09T02:34:31.343

@Pavel uuh... whoops. Fixed that. That was unintentional – pfg – 2018-08-09T02:46:28.900

5Is it guaranteed that each non-empty line contains exactly 1 =? – user202729 – 2018-08-09T05:19:19.683

@user202729 yes – pfg – 2018-08-09T17:58:16.890

3I'd be willing to bet that you actually need a solution to this problem and intend to use one to convert your files. :) – mbomb007 – 2018-08-09T19:24:06.927

What if the input contains \"? – pppery – 2018-08-09T21:31:10.683

No, Minecraft automatically does that – Redwolf Programs – 2018-08-09T22:00:24.247

I'd know, I'm an Ubernerd of the highest level. – Redwolf Programs – 2018-08-09T22:00:47.707

@ppperry it doesn't contain backslashes – pfg – 2018-08-15T22:23:26.187

Do we only need to handle English json? – l4m2 – 2018-10-26T10:15:09.867

@l4m2 english only is fine – pfg – 2018-10-26T15:02:21.463

Answers

4

Python 3, 91 77 bytes

-14 Bytes thanks to OMᗺ

I thought that the printout of a Python dictionary would be close enough to JSON to make it a very competitive language for this challenge. However, the string representation of python dictionaries is different enough from JSON that I had better luck using python's built-in JSON library. I'll bet this can be done more succinctly in JavaScript.

import json
f=lambda x:json.dumps(dict(i.split("=")for i in x.split("\n")if i))

Try it Online!


Edit:

Bash + Sed, 68 63 bytes

Bug fix thanks to OMᗺ and Night 2
-5 Bytes thanks to OMᗺ

I realized that it might be more byte efficient to directly convert the text to JSON without bundling it in an object, as was my approach for the python solution. Per byte, sed is the most powerful language for regex replacement that I know of.

echo {`echo "$1"|sed 's/"/\\\"/g;s/\(.*\)=\(.*\)/"\1":"\2",/'`}

Try it Online!

Explanation

echo {`                                  #  prints the leading curly brace
       echo "$1"|sed                     # feeds the input into sed
       's/"/\\"/g;                       # replaces " with \"
       s/\(.*\)=\(.*\)/"\1":"\2",/'      # surrounds the left and right hand sides of the equals with quotes and joins them with a colon
`}                                       # prints the closing curly brace

Zachary Cotton

Posted 2018-08-09T01:33:08.223

Reputation: 679

8If you're answering in two different languages, feel free to post that as two separate answers. – mbomb007 – 2018-08-09T19:23:07.413

For the bash+sed answer, try using the -r flag for sed (+3 bytes) so that you do not need to escape the capturing groups (-4 bytes) https://tio.run/##LYq7CgIxEEX7/YphUHygCVoqCxYWNoKNXQqzZtwNxo1kRkHUb4@62Jx74ZzKcpMzHZsIz0M32Jvhi8nBNMGANWpjDOp6yXqoxqPyB41mhgs0c5zoweGdcxYfSDmfRLX2QuX6@wrr7tvoSLVkE7Hsgn1QKvdMgKsrgkQQm2oS@Adw7YqiqG4@OCUxbnzdlBv6UiD4ixc4xQSd9m0NnqHPUIV4PPMH

– user41805 – 2018-08-28T15:26:55.743

4

Vim, 44 bytes

O{<Esc>:%s/"/\\"/g|%s/\v(.*)\=(.*)/"\1":"\2",
o}

Explanation:

O{<Esc>                                           Prepend {
       :%s/"/\\"/g                                Escape all "
                  |%s/\v(.*)\=(.*)/"\1":"\2",     Json-ify lines
o}                                                Append }

oktupol

Posted 2018-08-09T01:33:08.223

Reputation: 697

3

Rust, 150 bytes

|s:String|s.replace('"',"\\\"").split('\n').filter(|l|l.len()>0).map(|l|format!("\"")+&l.replace('=',"\":\"")+"\",").fold(format!("{{"),|r,n|r+&n)+"}"

Try it online!

Is it longer than Java?

Herman L

Posted 2018-08-09T01:33:08.223

Reputation: 3 611

2

Retina 0.8.2, 35 bytes

"
\"
=
": "
G`.
.+
    "$&",
^
{¶
$
¶}

Try it online! Would be 34 bytes in Retina 1 as you can use L$`.+ instead of G`. and .+. Explanation:

"
\"

Escape the quotes.

=
": "

Fix up the key/value separator. (If the value might contain a =, use 1`= at a cost of 2 bytes.)

G`.

Remove empty lines.

.+
    "$&",

Wrap each line in quotes. (The inner quotes were added earlier.)

^
{¶
$
¶}

Wrap the entire output in {}s.

Neil

Posted 2018-08-09T01:33:08.223

Reputation: 95 035

2

Perl 5 -nl -M5.010, 58 54 bytes

BEGIN{say'{'}s'"'\"'g;/=/&&say qq|"$`": "$'",|}{say'}'

Try it online!


58 byte version:

BEGIN{say'{'}s'"'\"'g;s/(.*)=(.*)/"$1": "$2",/;END{say'}'}

Try it online!

sundar - Reinstate Monica

Posted 2018-08-09T01:33:08.223

Reputation: 5 296

Both versions add a comma after every key:value pair, which is technically not compliant JSON (the final comma before the closing } should be omitted and will fail most strict JSON validators). Here's a quick 58-byte rewrite which produces valid (if uglier for human readers) JSON:

``$c||='{';s'"'\"'g;/=/&&say qq|$c"$`":"$'"|;$c=','}{say'}'``

I expect you can find something a bit shorter/more elegant. – mousetrapper – 2018-08-15T19:37:43.650

@mousetrapper That's a nice way to avoid the BEGIN. OP explicitly allows trailing commas though: "Trailing commas are allowed because Minecraft allows them.". Feel free to post that as a new answer, mentioning the difference. – sundar - Reinstate Monica – 2018-08-15T20:03:15.310

Ah, yes, good point, missed that sentence in the original post. The default assignment only makes sense if you're trying to vary the first character, otherwise your BEGIN is still shorter in the case where you just want to emit the '{'. I like your END-avoiding technique. I knew that -n placed an effective while(<>){} loop around your code; I had no idea just how literal that was. – mousetrapper – 2018-08-15T21:27:52.793

I was pretty surprised too, when I first found that out. It's one of those Perl features that straddles the line between a weird hack and a brilliant way to do TIMTOWDI. I had forgotten about it though, so credit for it in this instance goes to Dennis in the Perl 5 golfing tips thread.

– sundar - Reinstate Monica – 2018-08-15T21:43:51.650

2

Husk, 22 bytes

String manipulation is not really Husk's strength, but it did pretty well:

`J"{}"J',mȯJ':msx'=fI¶

Try it online!

                      ¶  -- split on newlines
                    fI   -- filter by identity (ie. remove empty strings)
         m(        )     -- with each line
                x'=      -- | split on '='
              ms         -- | show each (ie. enclose in quotes and escape quotes)
           J':           -- | join with ':'
      J',                -- join these with ','
`J"{}"                   -- join the string "{}" with the result

ბიმო

Posted 2018-08-09T01:33:08.223

Reputation: 15 345

Ironically, there is something called a "Husk" in Minecraft! – Redwolf Programs – 2018-08-09T22:01:31.030

2

JavaScript, 66 63 62 bytes

s=>JSON.stringify(o=/(.+)=(.+)/g,s.replace(o,(_,a,b)=>o[a]=b))

f=
s=>JSON.stringify(o=/(.+)=(.+)/g,s.replace(o,(_,a,b)=>o[a]=b))

console.log(
  f(`tile.dirt.name=Dirt
advMode.nearestPlayer=Use "@p" to target nearest player

build.tooHigh=Height limit for building is %s blocks`)
)

-3 bytes thanks to @redundancy

-1 byte thanks to @l4m2

darrylyeo

Posted 2018-08-09T01:33:08.223

Reputation: 6 214

2

Haskell, 75 71 bytes

-4 bytes thanks to Laikoni (using do-notation over list-comprehension)!

Works with multiple = on one line:

f s='{':do{(a,_:b)<-span(/='=')<$>lines s;show a++':':show b++","}++"}"

Try it online!

Explanation

The term span(/='=')<$>lines s splits the string on the first =, leaving us with ("<initial part>","=<remaining line>"). Doing a pattern-match (a,_:b) ensures that the line was not empty and at the same time removes the leading =.

Now we only need to show both a and b (enclosing it in quotes and escaping quotes), do some formatting (: and , characters) and finally enclose it in {}.

ბიმო

Posted 2018-08-09T01:33:08.223

Reputation: 15 345

1

71 bytes using do: Try it online!

– Laikoni – 2018-08-11T11:45:11.917

2

Ruby, 56 bytes

->x{x.split(?\n).map{|i|i.split(?=)}.to_h.to_json}

+6 bytes for -rjson interpreter flag.

Try it online!

dkudriavtsev

Posted 2018-08-09T01:33:08.223

Reputation: 5 781

1@Piccolo did you pass the -rjson flag? – pfg – 2018-08-10T16:28:47.427

@pfg Wow, I really dropped the ball on that haha. I had not only forgotten to use -rjson, but also assumed without actually checking that the error was the same one that I had gotten earlier involving to_h – Piccolo – 2018-08-10T18:03:18.430

2

C (gcc), 243 219 bytes

Thanks to ceilingcat for the suggestion.

I decided to use a state machine to handle the three cases (newline, key, value) and it turned out pretty well. Also, I got to abuse the fall-through feature of switch and the macro stringizing operator!

Although the challenge didn't require it, I also escaped the \ character per the JSON spec. If that character will never be in the input, then &&c-92 can be removed for 5 more bytes.

#define p(s)printf(#s,c)
#define a(i)case i:
c,s;f(){for(p({);(c=getchar())>0;)switch(s){a(0)if(c<11)break;s++,p(\42);a(1)c==61?s++,p(":"):p(%c);break;a(2)c-34&&c-92?c==10?p(\42\54),s=0:p(%c):p(\\%c);}s-2||p(\42);p(});}

Try it online!


Original submission: 243 bytes

The original submission kept unneeded spacing as in the provided JSON examples.

#define p(s)printf(s,c)
#define a(i)case i:
c,s;f(){for(p("{\n");(c=getchar())>0;)switch(s){a(0)if(c<11)break;s++,p("  \"");a(1)c==61?s++,p("\": \""):p("%c");break;a(2)c-34&&c-39?c==10?p("\",\n"),s=0:p("%c"):p("\\%c");}s==2&&p("\"\n");p("}");}

Try it online!

ErikF

Posted 2018-08-09T01:33:08.223

Reputation: 2 149

1

Retina 0.8.2, 43 bytes

¶¶
¶
"
\"
=
": "
m`$|^
"
m`^
    
$
¶}
^
{¶

Try it online!

Okx

Posted 2018-08-09T01:33:08.223

Reputation: 15 025

1

JavaScript (ES6), 66 bytes

s=>`{${s.replace(/"/g,'\\"').replace(/(.*)=(.*)/g,'"$1":"$2",')}}`

Assumes there's only one = per line

Testing snippet

f=s=>`{${s.replace(/"/g,'\\"').replace(/(.*)=(.*)/g,'"$1":"$2",')}}`
<textarea id="i" onkeyup="o.innerText=f(i.value)"></textarea><pre id="o">

Herman L

Posted 2018-08-09T01:33:08.223

Reputation: 3 611

Should be 66 bytes. \ might've been parsed as \ when counting length. – redundancy – 2018-08-10T07:49:25.497

1@redundancy I really should stop using "code".length in the javascript console to count the length – Herman L – 2018-08-12T18:19:48.260

1

Perl 6, 48 bytes

{to-json %(.lines.grep(?*)>>.split("=",2).flat)}

2 bytes less if we can assume exactly 1 equals sign on a non-empty line.

Try it online!

Ungolfed:

{                   # An anonymous block, taking 1 string which ends in $_.
    to-json         # Convert a Perl 6 number, string, list or hash to JSON and return it.
    %(              # Force to hash (dictionary)
        .lines      # Break $_ (implicitly assumed) into a list of lines.
        .grep(?*)   # Pick only those that are True (non-empty).
        >>.         # For each element in the list, call the following method ... 
        split("=",2) # ... split the string at =, making at most 2 chunks.
        .flat       # That gives a list of 2-element lists. Flatten it.
    )               # List is converted into the hash like this: { first element => second element, third => fourth, ... }
}                   # Implicitly return

By the way, the to-json routine is deprecated, as the compiler will tell you, but who cares.

Ramillies

Posted 2018-08-09T01:33:08.223

Reputation: 1 923

1

Python 2, 81 bytes

lambda s:'{'+re.sub(r'(.*)=(.*)',r'"\1":"\2",',re.sub('"',r'\"',s))+'}'
import re

Try it online!

TFeld

Posted 2018-08-09T01:33:08.223

Reputation: 19 246

1

Ruby, 59 + 5 = 64

Needs -rjson (+5)

->c{Hash[*c.split(?\n).map{|l|l.split ?=}.flatten].to_json}

Explanation:

->c{                                                      } # anonymous function with param c
    Hash[*                                       ]          # converts ["a", "b", "c", "d"] into {"a": "b", "c": "d"}
          c.split(?\n)                                      # splits c into lines
                      .map{|l|          }                   # map lines so each element represents
                              l.split ?=                    # an array of itself but split by =
                                         .flatten           # merges 2d array to 1d (also gets rid of empty elements for newlines
                                                  .to_json  # converts hash to json

Piccolo

Posted 2018-08-09T01:33:08.223

Reputation: 261

1

V, 30 bytes

O{␛Í"/\\"
ggòeÉ"vyf=Plp$pa,òo}

Expects one input at a time. The TIO snippet runs all given test cases as a single input.

I'm new to V's extended mappings, so tips are always welcome!

Try it online!

Explanation

O{␛                  # insert { on a new line above
   Í                 # global substitution across all lines
    "/\\"            #   " => \"
gg                   # go to first line
  ò                  # recursively...
   e                 #   forward to end of word; if at end of line, applies to next word below
    É"               #   prepend " to first non-whitespace char
      vy             #   copy current character (i.e. ")
        f=Plp        #   paste " before and after the next =
             $pa,    #   paste " at end of line and append ,
                 ò   # ...end
                  o} # insert } on a new line below

redundancy

Posted 2018-08-09T01:33:08.223

Reputation: 241

1

C (gcc), 172 bytes

#define p(s)printf(#s,c)
c,s;f(){for(p({);~(c=getchar());)s-2?c>10|s&&(s||(s+=p(\42)),c==61?s++,p(":"):p(%c)):c-34&&c-92?c==10?s=!p(\42\54):p(%c):p(\\%c);s-2||p(\42);p(});}

Try it online!

Based on @ErikF's implementation but without switch/case.

Slightly ungolfed version

#define p(s)printf(#s,c)
c,s;
f(){
 for(p({);~(c=getchar());)
  s-2?
   c>10|s&&(
    s||
     (s+=p(\42)),
    c==61?
     s++,
     p(":")
    :
     p(%c)
   )
  :
   c-34&&c-92?
    c==10?
     s=!p(\42\54)
    :
     p(%c)
   :
    p(\\%c);
 s-2||p(\42);
 p(});
}

ceilingcat

Posted 2018-08-09T01:33:08.223

Reputation: 5 503

1

R, 118 bytes

function(s){cat(paste("{",gsub("(.*)=(.*)","\"\\1\":\"\\2\",",gsub("\"","\\\\\"",gsub("\n{2,}","\n",s)),perl=T),"}"))}

Try it online!

Chaos Manor

Posted 2018-08-09T01:33:08.223

Reputation: 521

1

C (gcc), 119 bytes

#define p(s)printf(#s,c)
s;f(c){for(p({);~(c=getchar())|s;)c<11?s=s&&!p(","):c-61?s++||p(\42),p(\\u%04x):p(":");p(});}

Try it online!

l4m2

Posted 2018-08-09T01:33:08.223

Reputation: 5 985

1

PHP, 87 bytes

preg_match_all("/^(.*)=(.*)$/m",$argn,$m);echo json_encode(array_combine($m[1],$m[2]));

Run as pipe with -nR or try it online.

Insert \s before $/m for Windows linebreaks; \s* if linebreaks are uncertain.
Insert U after $/m if values contain =.

Titus

Posted 2018-08-09T01:33:08.223

Reputation: 13 814

1

Dart, 142 114 108 bytes

f(s)=>"""{${s.replaceAll('"','\\"').replaceAllMapped(RegExp(r'(.*)=(.*)'),(m)=>'"${m[1]}":"${m[2]}",')}}""";

Try it online!

  • -28 bytes by getting rid of the json.encode function and using regular string building
  • -6 bytes by removing the 'new' keyword and a few spaces
  • Elcan

    Posted 2018-08-09T01:33:08.223

    Reputation: 913