-
-
Notifications
You must be signed in to change notification settings - Fork 383
Expand file tree
/
Copy pathFieldCalculationMap.cs
More file actions
231 lines (199 loc) · 9.77 KB
/
Copy pathFieldCalculationMap.cs
File metadata and controls
231 lines (199 loc) · 9.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.Extensions.Logging;
using Microsoft.TeamFoundation.WorkItemTracking.Client;
using MigrationTools.Tools;
using MigrationTools.Tools.Infrastructure;
using NCalc;
namespace MigrationTools.FieldMaps.AzureDevops.ObjectModel
{
/// <summary>
/// Performs mathematical calculations on numeric fields using NCalc expressions during migration.
/// </summary>
public class FieldCalculationMap : FieldMapBase
{
/// <summary>
/// Initializes a new instance of the FieldCalculationMap class.
/// </summary>
/// <param name="logger">Logger for the field map operations</param>
/// <param name="telemetryLogger">Telemetry logger for tracking operations</param>
public FieldCalculationMap(ILogger<FieldCalculationMap> logger, ITelemetryLogger telemetryLogger)
: base(logger, telemetryLogger)
{
}
private FieldCalculationMapOptions Config { get { return (FieldCalculationMapOptions)_Config; } }
/// <summary>
/// Configures the field map with the specified options and validates required settings.
/// </summary>
/// <param name="config">The field map configuration options</param>
/// <exception cref="ArgumentNullException">Thrown when required fields are not specified</exception>
public override void Configure(IFieldMapOptions config)
{
base.Configure(config);
if (string.IsNullOrEmpty(Config.expression))
{
throw new ArgumentNullException(nameof(Config.expression), "The expression field must be specified.");
}
if (string.IsNullOrEmpty(Config.targetField))
{
throw new ArgumentNullException(nameof(Config.targetField), "The target field must be specified.");
}
if (Config.parameters == null || Config.parameters.Count == 0)
{
throw new ArgumentNullException(nameof(Config.parameters), "At least one parameter mapping must be specified.");
}
}
public override string MappingDisplayName => $"{Config.expression} -> {Config.targetField}";
internal override void InternalExecute(WorkItem source, WorkItem target)
{
try
{
// Validate that target field exists
if (!target.Fields.Contains(Config.targetField))
{
Log.LogWarning("FieldCalculationMap: Target field '{TargetField}' does not exist on work item {WorkItemId}. Skipping calculation.", Config.targetField, target.Id);
return;
}
// Validate that all source fields exist and collect their values
var parameterValues = new Dictionary<string, object>();
foreach (var parameter in Config.parameters)
{
if (!source.Fields.Contains(parameter.Value))
{
Log.LogWarning("FieldCalculationMap: Source field '{SourceField}' does not exist on work item {WorkItemId}. Skipping calculation.", parameter.Value, source.Id);
return;
}
var fieldValue = source.Fields[parameter.Value].Value;
if (fieldValue == null)
{
Log.LogWarning("FieldCalculationMap: Source field '{SourceField}' is null on work item {WorkItemId}. Skipping calculation.", parameter.Value, source.Id);
return;
}
// Convert field value to numeric
if (!TryConvertToNumeric(fieldValue, out var numericValue)) {
switch (Config.parsingFallback) {
case FieldCalculationMapParsingFallback.RaiseError:
Log.LogError(
"FieldCalculationMap: Source field '{SourceField}' with value '{FieldValue}' is not numeric on work item {WorkItemId}. Skipping calculation.",
parameter.Value, fieldValue, source.Id);
break;
case FieldCalculationMapParsingFallback.ResetToZero:
Log.LogWarning(
"FieldCalculationMap: Source field '{SourceField}' with value '{FieldValue}' is not numeric on work item {WorkItemId}. resetting the value to 0.0.",
parameter.Value, fieldValue, source.Id);
numericValue = 0.0;
break;
}
}
parameterValues[parameter.Key] = numericValue;
}
// Evaluate the expression
var expression = new Expression(Config.expression);
// Set parameters
foreach (var param in parameterValues)
{
expression.Parameters[param.Key] = param.Value;
}
// Evaluate and get result
var result = expression.Evaluate();
if (expression.HasErrors())
{
Log.LogError("FieldCalculationMap: Expression evaluation failed with error: {Error} for work item {WorkItemId}", expression.Error, source.Id);
return;
}
// Convert result to appropriate numeric type and set target field
if (TryConvertToTargetFieldType(result, target.Fields[Config.targetField], out var convertedResult))
{
target.Fields[Config.targetField].Value = convertedResult;
Log.LogDebug("FieldCalculationMap: Successfully calculated and set field '{TargetField}' to '{Result}' for work item {WorkItemId}", Config.targetField, convertedResult, target.Id);
}
else
{
Log.LogWarning("FieldCalculationMap: Could not convert calculation result '{Result}' to target field type for work item {WorkItemId}", result, target.Id);
}
}
catch (Exception ex)
{
Log.LogError(ex, "FieldCalculationMap: Unexpected error during calculation for work item {WorkItemId}", target.Id);
}
}
/// <summary>
/// Attempts to convert a field value to a numeric type.
/// </summary>
/// <param name="value">The field value to convert</param>
/// <param name="numericValue">The converted numeric value</param>
/// <returns>True if conversion was successful, false otherwise</returns>
private static bool TryConvertToNumeric(object value, out object numericValue)
{
numericValue = null;
if (value is int || value is long || value is decimal || value is double || value is float)
{
numericValue = value;
return true;
}
var stringValue = value.ToString().Trim();
// Try different numeric types
if (int.TryParse(stringValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
{
numericValue = intValue;
return true;
}
if (long.TryParse(stringValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longValue))
{
numericValue = longValue;
return true;
}
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
{
numericValue = doubleValue;
return true;
}
return false;
}
/// <summary>
/// Attempts to convert the calculation result to the appropriate type for the target field.
/// </summary>
/// <param name="result">The calculation result</param>
/// <param name="targetField">The target field</param>
/// <param name="convertedResult">The converted result</param>
/// <returns>True if conversion was successful, false otherwise</returns>
private static bool TryConvertToTargetFieldType(object result, Field targetField, out object convertedResult)
{
convertedResult = null;
try
{
// Check target field type and convert accordingly
var fieldType = targetField.FieldDefinition.FieldType;
switch (fieldType)
{
case FieldType.Integer:
if (result is double doubleResult)
{
convertedResult = (int)Math.Round(doubleResult);
}
else
{
convertedResult = Convert.ToInt32(result);
}
return true;
case FieldType.Double:
convertedResult = Convert.ToDouble(result);
return true;
case FieldType.String:
// Allow setting string fields with numeric results
convertedResult = result.ToString();
return true;
default:
// For other field types, try direct assignment
convertedResult = result;
return true;
}
}
catch (Exception)
{
return false;
}
}
}
}