Lost in Translation: Incorrectly Formatted Strings and Their Consequences

Bob has a task. He needs to generate 10 cubes at random positions in a Unity scene and save the positions in a text file. In a second Unity scene, the positions should be read from the file and 10 spheres should be created at the positions. Sounds easy. But when Alice wants to read the positions on her computer, everything goes wrong. What happened?

Disclaimer: If you already know about CultureInfo and how to use it, this article will be boring for you.


Here's how Bob creates the cubes and writes the result to the file:

using UnityEngine;
using System.IO;

public class WritePositions : MonoBehaviour
{
    string allPositions;
    Vector3 position;

    void Start()
    {
        Random.InitState(1);

        for (int i = 0; i < 10; i++)
        {
            // Create cube
            var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);

            // Get random position
            position.x = Random.value * 10f;
            position.y = Random.value * 10f;
            position.z = Random.value * 10f;

            // Set cube to the random position
            cube.transform.position = position;

            // Convert the random xyz position values to strings
            // Add a semicolon to each string as separator
            // Add the strings to an output string
            allPositions += position.x.ToString() + ";";
            allPositions += position.y.ToString() + ";";
            allPositions += position.z.ToString() + ";";
        }

        // Save the output string with all positions into a textfile
        string path = Application.dataPath + "/positions.txt";
        File.WriteAllText(path, allPositions);
    }
}

The result in his WritePositions scene looks like this:

The textfile now contains all xyz values of the positions. The semicolon ; is used as separator:

9.996847;7.742628;6.809838;4.604562;5.944274;7.847895;9.143838;1.373541;2.568918;5.561134;7.822797;3.288125;4.600458;1.619433;9.611027;8.372383;8.775043;8.264214;2.13082;0.7605303;3.155688;6.640184;2.439498;3.610996;4.159934;3.951687;9.655395;7.099103;1.337068;2.075161;

This seems fine to Bob. For the second part, he uses this code to read the positions and to create the spheres:

using UnityEngine;
using System.IO;

public class ReadPositions : MonoBehaviour
{
    public bool savePositions = true;

    string allPositions;
    Vector3 position;

    void Start()
    {
        // Read the positions from the textfile into a string
        string path = Application.dataPath + "/positions.txt";
        allPositions = File.ReadAllText(path);

        // Split the string into a string array
        // Use the semicolon as separator
        string[] splitted = allPositions.Split(';');

        // Iterate through the array to get the xyz values of the positions
        for (int i=0; i<splitted.Length-3; i+=3)
        {
            // Create sphere
            var sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);

            // Convert the splitted string to xyz floats
            float.TryParse(splitted[i], out position.x);
            float.TryParse(splitted[i+1], out position.y);
            float.TryParse(splitted[i+2], out position.z);

            // Set the position of the sphere to the resulting position
            sphere.transform.position = position;
        }
    }
}

The result in his ReadPositions scene looks like this:

Task completed! Bob checks in his changes and calls it a day.


Alice wants to take a look at Bob's solution and fires up Unity. She opens the ReadPositions scene and hits play:

Um... there's nothing. Did Bob do anything at all? Wait, there are spheres in the hierarchy and there's a warning in the inspector:

Ok, the position of this sphere seems to be random, but it's really large. Alice is sure that Bob didn't want it that way. She decides to open the text file to compare the values:

9.996847;7.742628;6.809838;4.604562;5.944274;7.847895;9.143838;1.373541;2.568918;5.561134;7.822797;3.288125;4.600458;1.619433;9.611027;8.372383;8.775043;8.264214;2.13082;0.7605303;3.155688;6.640184;2.439498;3.610996;4.159934;3.951687;9.655395;7.099103;1.337068;2.075161;

These values ​​make sense. But she notices that the decimal point is lost and that's why the values ​​in Unity are so big and the spheres are out of range.
9.996847 became 9996847 in the inspector.

Then it strikes to Alice. Bob is an US-American and she is German. They have set their computers to different languages and the number format in these languages differs. Let's take an example:

US:       1,234,567.89
Germany: 1.234.567,89

The floating point is represented as . in US and as , in Germany. They're swapped. Alice's computer does not interpret the point as a floating point, but as a thousands separator. The code must take this case into consideration.

By default, C# will use the computer's language setting when it converts numbers to strings and vice versa. In Bob's code, this happens with ToString() and TryParse(). What should he have done differently?


In C# there is the CultureInfo Class. It resides in the System.Globalization namespace. Bob can use its InvariantCulture property to ensure his conversions are consistent across different machines. It's a small change, but it makes a big difference.

In WritePositions:

// Include System.Globalization namespace
using System.Globalization;


// Convert the random xyz position values to strings

// Make sure that the conversions are consistent across different machines
// Add a semicolon to each string as separator
CultureInfo invariantCulture = CultureInfo.InvariantCulture;
allPositions += position.x.ToString(invariantCulture) + ";";
allPositions += position.y.ToString(invariantCulture) + ";";
allPositions += position.z.ToString(invariantCulture) + ";";

In ReadPositions:

// Include System.Globalization namespace
using System.Globalization;

// Convert the splitted string to xyz floats

// Make sure that the conversions are consistent across different machines

CultureInfo invariantCulture = CultureInfo.InvariantCulture;
NumberStyles style = NumberStyles.Float;

float.TryParse(splitted[i], style , invariantCulture, out position.x);
float.TryParse(splitted[i+1], style , invariantCulture, out position.y);
float.TryParse(splitted[i+2], style , invariantCulture, out position.z);

The NumberStyles Enum allows you to combine different style options. You can allow currency symbols, leading signs and other stuff. NumberStyle.Float style is a combination of AllowLeadingWhite, AllowTrailingWhite, AllowLeadingSign, AllowDecimalPoint, and AllowExponent styles.


Now everything works even on Alice's computer and Bob can relax. In his search for a solution he came across the documentation and realized that he can do some pretty cool stuff when formatting numbers, dates, and other types. Be sure to read these as well:

Overview: How to format numbers, dates, enums, and other types in .NET

NumberStyles Enum

I have stumbled across the described problem in various projects, especially in Editor Tools. In internal tools, such errors can remain undetected for a long time. If you ever come across such strange behavior, take a look at the code that is responsible for the conversion of strings. Maybe a poor, innocent floating point gets lost in translation.