Skip to content

Instantly share code, notes, and snippets.

@renaudbedard
Created August 24, 2016 12:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save renaudbedard/6923bc79cc60b72e76b4566781ee11ee to your computer and use it in GitHub Desktop.
Save renaudbedard/6923bc79cc60b72e76b4566781ee11ee to your computer and use it in GitHub Desktop.
using System;
using System.Globalization;
using UnityEngine;
namespace Yarn
{
// A value from inside Yarn.
public struct Value : IComparable, IComparable<Value>, IEquatable<Value>
{
internal readonly Type type;
// The underlying values for this object
internal readonly float numberValue;
internal readonly string variableName;
internal readonly string stringValue;
internal readonly bool boolValue;
public enum Type
{
Null, // the null value
Number, // a constant number
String, // a string
Bool, // a boolean value
Variable // the name of a variable; will be expanded at runtime
}
public static readonly Value NULL = new Value();
// Create as specific value type (mainly for variables)
public Value(Type type, string value)
{
// default values
numberValue = 0;
stringValue = null;
variableName = null;
boolValue = false;
this.type = type;
if (type == Type.String)
stringValue = value;
else if (type == Type.Variable)
variableName = value;
else
throw new InvalidOperationException("String value constructor expects a variable or string value type");
}
// Garbage-less simple constructors
public Value(float value)
{
variableName = null;
stringValue = null;
boolValue = false;
type = Type.Number;
numberValue = value;
}
public Value(string value)
{
variableName = null;
numberValue = 0;
boolValue = false;
type = Type.String;
stringValue = value;
}
public Value(bool value)
{
variableName = null;
stringValue = null;
numberValue = 0;
type = Type.Bool;
boolValue = value;
}
// Create a value with a C# object
public Value(object value)
{
// default values
numberValue = 0;
variableName = null;
stringValue = null;
boolValue = false;
type = Type.Null;
// Copy an existing value
if (value is Value)
{
var otherValue = (Value) value;
type = otherValue.type;
switch (type)
{
case Type.Number:
numberValue = otherValue.numberValue;
break;
case Type.String:
stringValue = otherValue.stringValue;
break;
case Type.Bool:
boolValue = otherValue.boolValue;
break;
case Type.Variable:
variableName = otherValue.variableName;
break;
case Type.Null:
break;
default:
throw new ArgumentOutOfRangeException();
}
return;
}
if (value == null)
{
type = Type.Null;
return;
}
if (value is string)
{
type = Type.String;
stringValue = Convert.ToString(value);
return;
}
if (value is int || value is float || value is double)
{
type = Type.Number;
numberValue = Convert.ToSingle(value);
return;
}
if (value is bool)
{
type = Type.Bool;
boolValue = Convert.ToBoolean(value);
return;
}
string error = string.Format("Attempted to create a Value using a {0}; currently, " +
"Values can only be numbers, strings, bools or null.", value.GetType().Name);
throw new YarnException(error);
}
object backingValue
{
get
{
switch (type)
{
case Type.Null:
return null;
case Type.String:
return stringValue;
case Type.Number:
return numberValue;
case Type.Bool:
return boolValue;
}
throw new InvalidOperationException(string.Format("Can't get good backing type for {0}", type));
}
}
public float AsNumber
{
get
{
switch (type)
{
case Type.Number:
return numberValue;
case Type.String:
try
{
return float.Parse(stringValue);
}
catch (FormatException)
{
return 0.0f;
}
case Type.Bool:
return boolValue ? 1.0f : 0.0f;
case Type.Null:
return 0.0f;
default:
throw new InvalidOperationException("Cannot cast to number from " + type);
}
}
}
public bool AsBool
{
get
{
switch (type)
{
case Type.Number:
return !float.IsNaN(numberValue) && !Mathf.Approximately(numberValue, 0);
case Type.String:
return !string.IsNullOrEmpty(stringValue);
case Type.Bool:
return boolValue;
case Type.Null:
return false;
default:
throw new InvalidOperationException("Cannot cast to bool from " + type);
}
}
}
public string AsString
{
get
{
switch (type)
{
case Type.Number:
if (float.IsNaN(numberValue))
return "NaN";
return numberValue.ToString(CultureInfo.InvariantCulture);
case Type.String:
return stringValue;
case Type.Bool:
return boolValue.ToString();
case Type.Null:
return "null";
default:
throw new ArgumentOutOfRangeException();
}
}
}
public int CompareTo(object obj)
{
if (obj == null) return 1;
// soft, fast coercion
if (!(obj is Value))
throw new ArgumentException("Object is not a Value");
var other = (Value) obj;
// it is a value!
return CompareTo(other);
}
public int CompareTo(Value other)
{
if (other.type == type)
{
switch (type)
{
case Type.Null:
return 0;
case Type.String:
return string.Compare(stringValue, other.stringValue, StringComparison.Ordinal);
case Type.Number:
return numberValue.CompareTo(other.numberValue);
case Type.Bool:
return boolValue.CompareTo(other.boolValue);
}
}
// try to do a string test at that point!
return string.Compare(AsString, other.AsString, StringComparison.Ordinal);
}
public bool Equals(Value other)
{
switch (type)
{
case Type.Number:
return Mathf.Approximately(AsNumber, other.AsNumber);
case Type.String:
return AsString == other.AsString;
case Type.Bool:
return AsBool == other.AsBool;
case Type.Null:
return other.type == Type.Null || Mathf.Approximately(other.AsNumber, 0) || other.AsBool == false;
default:
throw new ArgumentOutOfRangeException();
}
}
public override bool Equals(object obj)
{
if (!(obj is Value))
return false;
var other = (Value) obj;
return Equals(other);
}
public static bool operator==(Value a, Value b)
{
return a.Equals(b);
}
public static bool operator !=(Value a, Value b)
{
return !(a == b);
}
// override object.GetHashCode
public override int GetHashCode()
{
var backing = backingValue;
if (backing != null)
return backing.GetHashCode();
return 0;
}
public override string ToString()
{
return string.Format("[Value: type={0}, AsNumber={1}, AsBool={2}, AsString={3}]", type, AsNumber, AsBool, AsString);
}
public static Value operator +(Value a, Value b)
{
// catches:
// undefined + string
// number + string
// string + string
// bool + string
// null + string
if (a.type == Type.String || b.type == Type.String)
{
// we're headed for string town!
return new Value(a.AsString + b.AsString);
}
// catches:
// number + number
// bool (=> 0 or 1) + number
// null (=> 0) + number
// bool (=> 0 or 1) + bool (=> 0 or 1)
// null (=> 0) + null (=> 0)
if (a.type == Type.Number || b.type == Type.Number ||
(a.type == Type.Bool && b.type == Type.Bool) ||
(a.type == Type.Null && b.type == Type.Null))
{
return new Value(a.AsNumber + b.AsNumber);
}
throw new ArgumentException(string.Format("Cannot add types {0} and {1}.", a.type, b.type));
}
public static Value operator -(Value a, Value b)
{
if (a.type == Type.Number && (b.type == Type.Number || b.type == Type.Null) ||
b.type == Type.Number && (a.type == Type.Number || a.type == Type.Null))
{
return new Value(a.AsNumber - b.AsNumber);
}
throw new ArgumentException(string.Format("Cannot subtract types {0} and {1}.", a.type, b.type));
}
public static Value operator *(Value a, Value b)
{
if (a.type == Type.Number && (b.type == Type.Number || b.type == Type.Null) ||
b.type == Type.Number && (a.type == Type.Number || a.type == Type.Null))
{
return new Value(a.AsNumber * b.AsNumber);
}
throw new ArgumentException(string.Format("Cannot multiply types {0} and {1}.", a.type, b.type));
}
public static Value operator /(Value a, Value b)
{
if (a.type == Type.Number && (b.type == Type.Number || b.type == Type.Null) ||
b.type == Type.Number && (a.type == Type.Number || a.type == Type.Null))
{
return new Value(a.AsNumber / b.AsNumber);
}
throw new ArgumentException(string.Format("Cannot divide types {0} and {1}.", a.type, b.type));
}
public static Value operator -(Value a)
{
if (a.type == Type.Number)
return new Value(-a.AsNumber);
if (a.type == Type.Null &&
a.type == Type.String &&
(a.AsString == null || a.AsString.Trim() == ""))
{
return new Value(-0);
}
return new Value(float.NaN);
}
// Define the is greater than operator.
public static bool operator >(Value operand1, Value operand2)
{
return operand1.CompareTo(operand2) == 1;
}
// Define the is less than operator.
public static bool operator <(Value operand1, Value operand2)
{
return operand1.CompareTo(operand2) == -1;
}
// Define the is greater than or equal to operator.
public static bool operator >=(Value operand1, Value operand2)
{
return operand1.CompareTo(operand2) >= 0;
}
// Define the is less than or equal to operator.
public static bool operator <=(Value operand1, Value operand2)
{
return operand1.CompareTo(operand2) <= 0;
}
}
}
@renaudbedard
Copy link
Author

Notable changes :

  • Value-type instead of class
  • Added equality operators and IEquatable implementation
  • Immutable, so had to add a special constructor for Variables
  • Added a bunch of constructors that avoid boxing/unboxing
  • Changed the order of the Type enum so that Null is the default
  • Changed float == 0 test to use Mathf.Approximately

@desplesda
Copy link

Awesome!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment