Skip to content

Commit be7ad72

Browse files
emyllerclaude
andauthored
fix(Engine): Fix segment condition evaluation across all system locales (#175)
* Fix segment condition evaluation across all system locales Co-authored-by: Claude <noreply@anthropic.com> * Reduce repetition with InvariantConvert utility Co-authored-by: Claude <noreply@anthropic.com> * Fix locale-dependent formatting in tests Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ef5082b commit be7ad72

File tree

4 files changed

+62
-18
lines changed

4 files changed

+62
-18
lines changed

Flagsmith.Engine/Segment/Evaluator.cs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ private static FlagResult<FeatureMetadataT> GetFlagResult<_, FeatureMetadataT>(E
250250
Enabled = featureContext.Enabled,
251251
Value = variant.Value,
252252
Metadata = featureContext.Metadata,
253-
Reason = $"SPLIT; weight={weight}",
253+
Reason = FormattableString.Invariant($"SPLIT; weight={weight}"),
254254
};
255255
break;
256256
}
@@ -373,7 +373,7 @@ static bool longOperations(long traitValue, SegmentConditionModel condition)
373373
long conditionValue;
374374
try
375375
{
376-
conditionValue = Convert.ToInt64(condition.Value);
376+
conditionValue = InvariantConvert.ToInt64(condition.Value);
377377
}
378378
catch (FormatException)
379379
{
@@ -396,12 +396,12 @@ static bool intOperations(long traitValue, SegmentConditionModel condition)
396396
{
397397
switch (condition.Operator)
398398
{
399-
case Constants.Equal: return traitValue == Convert.ToInt32(condition.Value);
400-
case Constants.NotEqual: return traitValue != Convert.ToInt32(condition.Value);
401-
case Constants.GreaterThan: return traitValue > Convert.ToInt32(condition.Value);
402-
case Constants.GreaterThanInclusive: return traitValue >= Convert.ToInt32(condition.Value);
403-
case Constants.LessThan: return traitValue < Convert.ToInt32(condition.Value);
404-
case Constants.LessThanInclusive: return traitValue <= Convert.ToInt32(condition.Value);
399+
case Constants.Equal: return traitValue == InvariantConvert.ToInt32(condition.Value);
400+
case Constants.NotEqual: return traitValue != InvariantConvert.ToInt32(condition.Value);
401+
case Constants.GreaterThan: return traitValue > InvariantConvert.ToInt32(condition.Value);
402+
case Constants.GreaterThanInclusive: return traitValue >= InvariantConvert.ToInt32(condition.Value);
403+
case Constants.LessThan: return traitValue < InvariantConvert.ToInt32(condition.Value);
404+
case Constants.LessThanInclusive: return traitValue <= InvariantConvert.ToInt32(condition.Value);
405405
case Constants.In: return condition.Value.Split(',').Contains(traitValue.ToString());
406406
default: throw new ArgumentException("Invalid Operator");
407407
}
@@ -411,12 +411,12 @@ static bool doubleOperations(double traitValue, SegmentConditionModel condition)
411411
{
412412
switch (condition.Operator)
413413
{
414-
case Constants.Equal: return traitValue == Convert.ToDouble(condition.Value);
415-
case Constants.NotEqual: return traitValue != Convert.ToDouble(condition.Value);
416-
case Constants.GreaterThan: return traitValue > Convert.ToDouble(condition.Value);
417-
case Constants.GreaterThanInclusive: return traitValue >= Convert.ToDouble(condition.Value);
418-
case Constants.LessThan: return traitValue < Convert.ToDouble(condition.Value);
419-
case Constants.LessThanInclusive: return traitValue <= Convert.ToDouble(condition.Value);
414+
case Constants.Equal: return traitValue == InvariantConvert.ToDouble(condition.Value);
415+
case Constants.NotEqual: return traitValue != InvariantConvert.ToDouble(condition.Value);
416+
case Constants.GreaterThan: return traitValue > InvariantConvert.ToDouble(condition.Value);
417+
case Constants.GreaterThanInclusive: return traitValue >= InvariantConvert.ToDouble(condition.Value);
418+
case Constants.LessThan: return traitValue < InvariantConvert.ToDouble(condition.Value);
419+
case Constants.LessThanInclusive: return traitValue <= InvariantConvert.ToDouble(condition.Value);
420420
case Constants.In: return false;
421421
default: throw new ArgumentException("Invalid Operator");
422422
}

Flagsmith.Engine/Segment/Models/SegmentConditionModel.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Text.RegularExpressions;
22
using System;
3+
using System.Globalization;
34
using Newtonsoft.Json;
45
namespace FlagsmithEngine.Segment.Models
56
{
@@ -21,10 +22,10 @@ public bool EvaluateModulo(string traitValue)
2122
string[] parts = this.Value.Split('|');
2223
if (parts.Length != 2) { return false; }
2324

24-
double divisor = Convert.ToDouble(parts[0]);
25-
double remainder = Convert.ToDouble(parts[1]);
25+
double divisor = Convert.ToDouble(parts[0], CultureInfo.InvariantCulture);
26+
double remainder = Convert.ToDouble(parts[1], CultureInfo.InvariantCulture);
2627

27-
return Convert.ToDouble(traitValue) % divisor == remainder;
28+
return Convert.ToDouble(traitValue, CultureInfo.InvariantCulture) % divisor == remainder;
2829
}
2930
catch (FormatException)
3031
{
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System;
2+
using System.Globalization;
3+
4+
namespace FlagsmithEngine.Utils
5+
{
6+
/// <summary>
7+
/// Provides culture-invariant conversion methods for numeric types.
8+
/// </summary>
9+
/// <remarks>
10+
/// Ensures consistent numeric parsing across all system locales by using InvariantCulture.
11+
/// Prevents locale-specific decimal separator issues (e.g., "1.23" vs "1,23").
12+
/// </remarks>
13+
internal static class InvariantConvert
14+
{
15+
/// <summary>
16+
/// Converts the string representation of a number to its 32-bit signed integer equivalent
17+
/// using culture-invariant formatting.
18+
/// </summary>
19+
/// <param name="value">A string containing a number to convert.</param>
20+
/// <returns>A 32-bit signed integer equivalent to the number in value.</returns>
21+
public static int ToInt32(string value) =>
22+
Convert.ToInt32(value, CultureInfo.InvariantCulture);
23+
24+
/// <summary>
25+
/// Converts the string representation of a number to its 64-bit signed integer equivalent
26+
/// using culture-invariant formatting.
27+
/// </summary>
28+
/// <param name="value">A string containing a number to convert.</param>
29+
/// <returns>A 64-bit signed integer equivalent to the number in value.</returns>
30+
public static long ToInt64(string value) =>
31+
Convert.ToInt64(value, CultureInfo.InvariantCulture);
32+
33+
/// <summary>
34+
/// Converts the string representation of a number to its double-precision floating-point equivalent
35+
/// using culture-invariant formatting.
36+
/// </summary>
37+
/// <param name="value">A string containing a number to convert.</param>
38+
/// <returns>A double-precision floating-point number equivalent to the numeric value in value.</returns>
39+
public static double ToDouble(string value) =>
40+
Convert.ToDouble(value, CultureInfo.InvariantCulture);
41+
}
42+
}

Flagsmith.EngineTest/Unit/Traits/TraitSchemaTest.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Globalization;
34
using System.Text;
45
using Xunit;
56
using Newtonsoft.Json.Linq;
@@ -30,7 +31,7 @@ public void TestTraitToJobject(TraitModel trait)
3031
var jObject = JObject.FromObject(trait);
3132
Assert.Equal(trait.TraitKey, jObject["trait_key"].Value<string>());
3233
var valueToken = jObject["trait_value"];
33-
var value = valueToken.Type != JTokenType.Null ? Convert.ChangeType(valueToken.Value<string>(), trait.TraitValue.GetType()) : null;
34+
var value = valueToken.Type != JTokenType.Null ? Convert.ChangeType(valueToken.Value<string>(), trait.TraitValue.GetType(), CultureInfo.InvariantCulture) : null;
3435
Assert.Equal(trait.TraitValue, value);
3536
}
3637
public static IEnumerable<object[]> TestTraitToJobjectData() =>

0 commit comments

Comments
 (0)