Projekt

Obecné

Profil

« Předchozí | Další » 

Revize 1407c8ba

Přidáno uživatelem Alex Konig před téměř 4 roky(ů)

re #8840 Finishing up Json parser

Zobrazit rozdíly:

Server/ServerApp/Parser/OutputInfo/JisInfo.cs
1
using System;
2
using System.Collections.Generic;
3
using System.Linq;
4
using System.Text;
5
using System.Threading.Tasks;
6

  
7
namespace Parser.OutputInfo
8
{
9
    class JisInfo
10
    {
11
        string faculty;
12
        int amount;
13
        DateTime startTime;
14
        int intervalLength;
15

  
16
        public JisInfo(string faculty, int amount, DateTime startTime, int intervalLength)
17
        {
18
            this.faculty = faculty;
19
            this.amount = amount;
20
            this.startTime = startTime;
21
            this.intervalLength = intervalLength;
22
        }
23

  
24
        public override string ToString()
25
        {
26
            return $"{startTime.ToString()} \t {faculty} \t {amount}";
27
        }
28
    }
29
}
Server/ServerApp/Parser/OutputInfo/LogInInfo.cs
1
using System;
2
using System.Collections.Generic;
3
using System.Linq;
4
using System.Text;
5
using System.Threading.Tasks;
6

  
7
namespace Parser.OutputInfo
8
{
9
    class LogInInfo
10
    {
11
        string building;
12
        int amount;
13
        DateTime startTime;
14
        int intervalLength;
15

  
16
        public LogInInfo(string building, int amount, DateTime startTime, int intervalLength)
17
        {
18
            this.building = building;
19
            this.amount = amount;
20
            this.startTime = startTime;
21
            this.intervalLength = intervalLength;
22
        }
23

  
24
        public override string ToString()
25
        {
26
            return $"{startTime} \t {building} \t {amount}";
27
        }
28
    }
29
}
Server/ServerApp/Parser/OutputInfo/LumInfo.cs
1
using System;
2
using System.Collections.Generic;
3
using System.Linq;
4
using System.Text;
5
using System.Threading.Tasks;
6

  
7
namespace Parser.OutputInfo
8
{
9
    class LumInfo
10
    {
11
        // in lux
12
        double value;
13
        DateTime startTime;
14
        int intervalLength;
15

  
16
        public LumInfo(double value, DateTime startTime, int intervalLength)
17
        {
18
            this.value = value;
19
            this.startTime = startTime;
20
            this.intervalLength = intervalLength;
21
        }
22

  
23
        public override string ToString()
24
        {
25
            return $"{startTime.ToString()} \t {value}";
26
        }
27
    }
28
}
Server/ServerApp/Parser/OutputInfo/RainInfo.cs
1
using System;
2

  
3
namespace Parser.OutputInfo
4
{
5
    class RainInfo
6
    {
7
        // probability in %
8
        double value;
9
        DateTime startTime;
10
        int intervalLength;
11

  
12
        public RainInfo(double value, DateTime startTime, int intervalLength)
13
        {
14
            this.value = value;
15
            this.startTime = startTime;
16
            this.intervalLength = intervalLength;
17
        }
18

  
19
        public override string ToString()
20
        {
21
            return $"{startTime.ToString()} \t {value}";
22
        }
23
    }
24
}
Server/ServerApp/Parser/OutputInfo/TempInfo.cs
1
using System;
2

  
3
namespace Parser.OutputInfo
4
{
5
    class TempInfo
6
    {
7
        // in °C
8
        double value;
9
        DateTime startTime;
10
        int intervalLength;
11

  
12
        public TempInfo(double value, DateTime startTime, int intervalLength)
13
        {
14
            this.value = value;
15
            this.startTime = startTime;
16
            this.intervalLength = intervalLength;
17
        }
18

  
19
        public override string ToString()
20
        {
21
            return $"{startTime.ToString()} \t {value}";
22
        }
23
    }
24
}
Server/ServerApp/Parser/OutputInfo/WeatherConditions.cs
13 13
    /// Transfers lux values to weather conditions and vice versa
14 14
    /// </summary>
15 15
    /// <author>Alex Konig</author>
16
    public static class LuxToConditions
16
    public static class ValueToConditions
17 17
    {
18 18
        /// <summary>
19 19
        /// Lux to weather conditions
......
26 26
                return WeatherConditions.Sunny;
27 27

  
28 28
            if (lux > 20_000)
29
                return WeatherConditions.Overcast;
29
                return WeatherConditions.Cloudy;
30 30

  
31 31
            // TODO tohle bylo 10_000, jak mi to má vycházet, zkusit na hourly data
32 32
            if (lux != 0)
33
                return WeatherConditions.Cloudy;
33
                return WeatherConditions.Overcast;
34 34

  
35 35
            return WeatherConditions.Dark;
36 36
        }
......
43 43
        internal static double TransferConditionsToLux(WeatherConditions condition)
44 44
        {
45 45
            if (condition == WeatherConditions.Sunny)
46
                return 50_000;
46
                return 60_000;
47 47

  
48 48
            if (condition == WeatherConditions.Cloudy)
49 49
                return 30_000;
50 50

  
51 51
            if (condition == WeatherConditions.Overcast)
52
                return 15_000;
52
                return 10_000;
53 53

  
54 54
            if (condition == WeatherConditions.Dark)
55
                return 5_000;
55
                return 0;
56 56

  
57 57
            return -1;
58 58
        }
59

  
60
        internal static WeatherConditions CloudCoverToConditions(int cloudCover)
61
        {
62
            if (cloudCover >= 66)
63
                return WeatherConditions.Overcast;
64

  
65
            if (cloudCover >= 33)
66
                return WeatherConditions.Cloudy;
67

  
68
            if (cloudCover >= 0)
69
                return WeatherConditions.Sunny;
70

  
71
            return WeatherConditions.Dark;
72
        }
59 73
    }
60 74
}
Server/ServerApp/Parser/OutputInfo/WeatherInfo.cs
21 21
        public WeatherConditions condition;
22 22

  
23 23
        /// <summary> Start of interval </summary>
24
        DateTime startTime;
24
        public DateTime startTime;
25 25
        /// <summary> Length of interval in hours </summary>
26
        int intervalLength;
26
        public int intervalLength;
27

  
28
        public WeatherInfo()
29
        {
30
            condition = WeatherConditions.Dark;
31
        }
27 32

  
28 33
        /// <summary>
29 34
        /// Constructor
......
43 48
            this.lum = lum;
44 49
            this.intervalLength = intervalLength;
45 50

  
46
            condition = LuxToConditions.TransferLuxToConditions(lum);
51
            condition = ValueToConditions.TransferLuxToConditions(lum);
47 52
        }
48 53

  
49 54
        /// <summary>
......
64 69
            this.condition = condition;
65 70
            this.intervalLength = intervalLength;
66 71

  
67
            lum = LuxToConditions.TransferConditionsToLux(condition);
72
            lum = ValueToConditions.TransferConditionsToLux(condition);
68 73
        }
69 74

  
70 75
        /// <summary>
......
73 78
        /// <returns>interval start temperature probability of rain wind    weather condition</returns>
74 79
        public override string ToString()
75 80
        {
76
            return $"{startTime.ToString()} \t {temp}°C \t {rain}% \t {wind}m/s \t {condition.ToString()} \t {lum}";
81
            return $"{startTime} \t {temp}°C \t {rain}% \t {wind}m/s \t {condition.ToString()} \t {lum}";
77 82
        }
78 83

  
79 84
    }
Server/ServerApp/Parser/OutputInfo/WindInfo.cs
1
using System;
2

  
3
namespace Parser.OutputInfo
4
{
5
    class WindInfo
6
    {
7
        // in m/s
8
        double value;
9
        DateTime startTime;
10
        int intervalLength;
11

  
12
        public WindInfo(double value, DateTime startTime, int intervalLength)
13
        {
14
            this.value = value;
15
            this.startTime = startTime;
16
            this.intervalLength = intervalLength;
17
        }
18

  
19
        public override string ToString()
20
        {
21
            return $"{startTime.ToString()} \t {value}";
22
        }
23
    }
24
}
Server/ServerApp/Parser/Parsers/JsonParser.cs
1 1
using ServerApp.DataDownload;
2
using ServerApp.Parser.OutputInfo;
2 3
using System;
3 4
using System.Collections.Generic;
5
using System.Globalization;
4 6
using System.IO;
5
using System.Linq;
6 7
using System.Net;
7
using System.Text;
8 8
using System.Text.Json;
9
using System.Threading.Tasks;
10

  
9
using static System.Text.Json.JsonElement;
11 10

  
12 11
namespace ServerApp.Parser.Parsers
13 12
{
13
    /// <summary>
14
    /// Class representing a parser for json prediction data
15
    /// </summary>
16
    /// <author>A. Konig</author>
14 17
    class JsonParser
15 18
    {
19
        /// <summary> Current weather </summary>
20
        public WeatherInfo current;
21
        /// <summary> Prediction for today, tommorrow and day after tommorrow </summary>
22
        public List<WeatherInfo> predictions;
23
        
24
        /// <summary> Data loader </summary>
16 25
        DataDownloader loader;
26
        /// <summary> Currently parsed day </summary>
27
        DateTime currParsedDay;
28
        /// <summary> Sunrise time of currently parsed day </summary>
29
        DateTime sunriseTime;
30
        /// <summary> Sunset time of currently parsed day </summary>
31
        DateTime sunsetTime;
17 32

  
33
        /// <summary>
34
        /// Constructor
35
        /// </summary>
36
        /// <param name="loader"></param>
18 37
        public JsonParser(DataDownloader loader)
19 38
        {
20 39
            this.loader = loader;
21 40
        }
22 41

  
23

  
42
        /// <summary>
43
        /// Parse weather prediction
44
        /// Results is in attributes current for current weather and pred for weather prediction for today, tommorrow and day after tommorrow
45
        /// </summary>
24 46
        public void ParsePrediction()
25 47
        {
26 48
            // TODO ask DataDownloader for download said file and return path to it
49
            
27 50
            // get file
28 51
            string file = DownloadWeatherPrediction();
29 52
            DateTime now = DateTime.Now;
30 53
            Console.WriteLine(File.Exists(file));
31 54

  
32 55
            // parse file
33
            string data =  File.ReadAllText(file);
56
            string data = File.ReadAllText(file);
34 57

  
35 58
            JsonDocument doc = JsonDocument.Parse(data);
36 59
            JsonElement root = doc.RootElement;
60
            var weatherP = root.EnumerateObject();
61
            predictions = new List<WeatherInfo>();
37 62

  
38
            var users = root.EnumerateObject();
39
            while (users.MoveNext())
63
            while (weatherP.MoveNext())
40 64
            {
41
                var user = users.Current;
42
                Console.WriteLine(user.Name);
43
                
44
                /*
45
                var props = user.EnumerateObject();
65
                string name = weatherP.Current.Name;
66
                Console.WriteLine(name);
46 67

  
47
                while (props.MoveNext())
68
                switch (name)
48 69
                {
49
                    var prop = props.Current;
50
                    Console.WriteLine($"{prop.Name}: {prop.Value}");
70
                    // current weather
71
                    case "current_condition":
72
                        {
73
                            ArrayEnumerator currentWeather = weatherP.Current.Value.EnumerateArray();
74
                            current = ParseCurrentWeather(currentWeather);
75

  
76
                            break;
77

  
78
                        }
79
                    // weather prediction
80
                    case "weather":
81
                        {
82
                            ArrayEnumerator weather = weatherP.Current.Value.EnumerateArray();
83
                            ParseWeatherPredict(weather);
84

  
85
                            break;
86
                        }
51 87
                }
52
                */
53 88
            }
54 89

  
90
            // sunrise + sunset into data
91
            EncompassSunRiseSetTimes();
92

  
93
            TestConsoleOutput();
94
        }
95

  
96
        private void TestConsoleOutput()
97
        {
98
            Console.WriteLine(current);
99
            foreach (WeatherInfo w in predictions)
100
                Console.WriteLine(w);
55 101
        }
56 102

  
103
        // TODO move to data loader
104
        /// <summary>
105
        /// Downloads json file
106
        /// </summary>
107
        /// <returns> Path to file </returns>
57 108
        private string DownloadWeatherPrediction()
58 109
        {
59 110
            DateTime now = DateTime.Now;
60 111
            WebClient webClient = new WebClient();
61
            webClient.DownloadFile("http://wttr.in/Plzen,czechia?format=j1", $"data/weather/{now.Year}{now.Month}{now.Day}.json");
112
            webClient.DownloadFile("http://wttr.in/Plzen,czechia?format=j1", $"data/{now.Year}{now.Month}{now.Day}.json");
113

  
114
            return $"data/{now.Year}{now.Month}{now.Day}.json";
115
        }
116

  
117
        /// <summary>
118
        /// Change data in a way that they now reflect sunrise and sunset times
119
        /// If current time under sunrise or over sunset -> WeatherConditions is Dark
120
        /// If prediction time is under sunrise or over sunset and more than half of the interval is under/over said time -> WeatherCondition is Dark
121
        /// </summary>
122
        private void EncompassSunRiseSetTimes()
123
        {
124
            // change current weather
125
            if ((current.startTime.TimeOfDay > sunsetTime.TimeOfDay) || (current.startTime.TimeOfDay < sunriseTime.TimeOfDay))
126
                current.condition = WeatherConditions.Dark;
127

  
128
            // change prediction
129
            for (int i = 0; i < predictions.Count - 1; i++)
130
            {
131
                // TODO what about when days change -> detect end / start of day and do differently ?
132

  
133
                WeatherInfo w = predictions[i];
134
                WeatherInfo wNext = predictions[i + 1];
135

  
136
                int timespan = wNext.startTime.Hour - w.startTime.Hour;
137
                w.intervalLength = timespan;
138

  
139
                // if start under sunset
140
                if (w.startTime.TimeOfDay > sunsetTime.TimeOfDay)
141
                    w.condition = WeatherConditions.Dark;
142

  
143
                // if start under sunrise
144
                if (w.startTime.TimeOfDay < sunriseTime.TimeOfDay)
145
                {
146
                    double howMuch = ((sunriseTime.Hour * 60 + sunriseTime.Minute) - (w.startTime.Hour * 60 + w.startTime.Minute)) / 60.0;
147

  
148
                    if (howMuch >= timespan / 2.0)
149
                        w.condition = WeatherConditions.Dark;
150
                }
151

  
152
                // if start under sunrise
153
                TimeSpan endTime = new TimeSpan(w.startTime.TimeOfDay.Hours + timespan, w.startTime.TimeOfDay.Minutes, 0);
154
                if (endTime > sunsetTime.TimeOfDay)
155
                {
156
                    double howMuch = ((endTime.Hours * 60 + endTime.Minutes) - (sunsetTime.Hour * 60 + sunsetTime.Minute)) / 60.0;
157

  
158
                    if (howMuch >= timespan / 2.0)
159
                        w.condition = WeatherConditions.Dark;
160
                }
161
            }
162

  
163
            // last prediction
164
            WeatherInfo wLast = predictions[predictions.Count - 1];
165
            TimeSpan endTimeW = new TimeSpan(24, 0, 0);
166
            int timespanLast = endTimeW.Hours - wLast.startTime.Hour;
167
            wLast.intervalLength = timespanLast;
168

  
169
            // if start under sunset
170
            if (wLast.startTime.TimeOfDay > sunsetTime.TimeOfDay)
171
                wLast.condition = WeatherConditions.Dark;
172

  
173
            // if start under sunrise
174
            if (wLast.startTime.TimeOfDay < sunriseTime.TimeOfDay)
175
            {
176
                double howMuch = ((sunriseTime.Hour * 60 + sunriseTime.Minute) - (wLast.startTime.Hour * 60 + wLast.startTime.Minute)) / 60.0;
177

  
178
                if (howMuch >= timespanLast / 2.0)
179
                    wLast.condition = WeatherConditions.Dark;
180
            }
181

  
182
            // if start under sunrise
183
            if (endTimeW > sunsetTime.TimeOfDay)
184
            {
185
                double howMuch = ((endTimeW.Hours * 60 + endTimeW.Minutes) - (sunsetTime.Hour * 60 + sunsetTime.Minute)) / 60.0;
186

  
187
                if (howMuch >= timespanLast / 2.0)
188
                    wLast.condition = WeatherConditions.Dark;
189
            }
190
        }
191

  
192
        /// <summary>
193
        /// Parse weather prediction
194
        /// </summary>
195
        /// <param name="weather"> ArrayEnumerator of weather predictions </param>
196
        private void ParseWeatherPredict(ArrayEnumerator weather)
197
        {
198
            while (weather.MoveNext())
199
            {
200
                // prediction for one day
201
                var obj = weather.Current.EnumerateObject();
202

  
203
                while (obj.MoveNext())
204
                {
205
                    switch (obj.Current.Name)
206
                    {
207
                        case "date":
208
                            {
209
                                DateTime.TryParseExact(obj.Current.Value.GetString(), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out currParsedDay);
210
                                break;
211
                            }
212
                        // hourly predictions
213
                        case "hourly":
214
                            {
215
                                ParseHourly(obj.Current.Value.EnumerateArray());
216
                                break;
217
                            }
218
                        // sunset / sunrise hours
219
                        case "astronomy":
220
                            {
221
                                ParseAstronomy(obj.Current.Value.EnumerateArray());
222
                                break;
223
                            }
224
                    }
225
                }
226
            }
227
        }
228

  
229
        /// <summary>
230
        /// Parse sunrise and sunset times
231
        /// </summary>
232
        /// <param name="astronomy"> Astronomy array enumerator </param>
233
        private void ParseAstronomy(ArrayEnumerator astronomy)
234
        {
235
            while (astronomy.MoveNext())
236
            {
237
                var astrInfo = astronomy.Current.EnumerateObject();
238
                
239
                while (astrInfo.MoveNext())
240
                {
241
                    switch (astrInfo.Current.Name)
242
                    {
243
                        case "sunrise":
244
                            {
245
                                DateTime.TryParseExact(astrInfo.Current.Value.GetString(), "hh:mm tt", CultureInfo.InvariantCulture, DateTimeStyles.None, out sunriseTime);
246
                                Console.WriteLine("\t sunrise time : " + sunriseTime + " " + astrInfo.Current.Value.GetString());
247
                                break;
248
                            }
249
                        case "sunset":
250
                            {
251
                                DateTime.TryParseExact(astrInfo.Current.Value.GetString(), "hh:mm tt", CultureInfo.InvariantCulture, DateTimeStyles.None, out sunsetTime);
252
                                Console.WriteLine("\t sunset time : " + sunsetTime + " " + astrInfo.Current.Value.GetString());
253
                                break;
254
                            }
255
                    }
256
                }
257
            }
258
        }
259

  
260
        /// <summary>
261
        /// Parse hourly predictions
262
        /// </summary>
263
        /// <param name="hourly">Enumerated array of hourly predictions</param>
264
        private void ParseHourly(ArrayEnumerator hourly)
265
        {
266
            while (hourly.MoveNext())
267
            {
268
                // one hourly prediction
269
                var oneH = hourly.Current.EnumerateObject();
270
                WeatherInfo weather = new WeatherInfo();
271

  
272
                while (oneH.MoveNext())
273
                {
274
                    switch (oneH.Current.Name)
275
                    {
276
                        case "FeelsLikeC":
277
                            {
278
                                Double.TryParse(oneH.Current.Value.GetString(), out weather.temp);
279
                                break;
280
                            }
281
                        case "cloudcover":
282
                            {
283
                                int cloudCover;
284
                                Int32.TryParse(oneH.Current.Value.GetString(), out cloudCover);
285
                                weather.condition = ValueToConditions.CloudCoverToConditions(cloudCover);
286
                                break;
287
                            }
288
                        // take into account highest value from "chanceofrain" and "chaceofsnow"
289
                        case "chanceofrain":
290
                            {
291
                                int rain;
292
                                Int32.TryParse(oneH.Current.Value.GetString(), out rain);
293
                                weather.rain = rain > weather.rain ? rain : weather.rain;
294
                                break;
295
                            }
296
                        case "chanceofsnow":
297
                            {
298
                                int snow;
299
                                Int32.TryParse(oneH.Current.Value.GetString(), out snow);
300
                                weather.rain = snow > weather.rain ? snow : weather.rain;
301
                                break;
302
                            }
303
                        // wind kmph has to be translated to mps
304
                        case "WindGustKmph":
305
                            {
306
                                double windkmh;
307
                                Double.TryParse(oneH.Current.Value.GetString(), out windkmh);
308
                                weather.wind= windkmh * 1000 / (60.0*60.0);
309
                                break;
310
                            }
311
                        case "time":
312
                            {
313
                                int h;
314
                                Int32.TryParse(oneH.Current.Value.GetString(), out h);
315
                                h /= 100;
316
                                DateTime time = new DateTime(currParsedDay.Year, currParsedDay.Month, currParsedDay.Day, h, 0, 0);
317
                                weather.startTime = time;
318
                                break;
319
                            }
320
                    }
321
                }
322

  
323
                // Console.WriteLine(weather.ToString());
324
                predictions.Add(weather);
325

  
326
            }
62 327

  
63
            return $"data/weather/{now.Year}{now.Month}{now.Day}.json";
64 328
        }
329

  
330
        /// <summary>
331
        /// Parse current weather
332
        /// </summary>
333
        /// <param name="currentWeather">Enumerated hour of weather data</param>
334
        /// <returns>WeatherInfo with current weather</returns>
335
        private WeatherInfo ParseCurrentWeather(ArrayEnumerator currentWeather)
336
        {
337
            WeatherInfo res = new WeatherInfo();
338
            //res.current = true;
339

  
340
            while (currentWeather.MoveNext())
341
            {
342
                var obj = currentWeather.Current.EnumerateObject();
343

  
344
                while (obj.MoveNext())
345
                {
346
                    switch (obj.Current.Name)
347
                    {
348
                        case "localObsDateTime":
349
                            {
350
                                DateTime.TryParse(obj.Current.Value.GetString(), out res.startTime);
351
                                break;
352
                            }
353
                        case "FeelsLikeC":
354
                            {
355
                                Double.TryParse(obj.Current.Value.GetString(), out res.temp);
356
                                break;
357
                            }
358
                        case "cloudcover":
359
                            {
360
                                int cloudCover;
361
                                Int32.TryParse(obj.Current.Value.GetString(), out cloudCover);
362
                                res.condition = ValueToConditions.CloudCoverToConditions(cloudCover);
363
                                break;
364
                            }
365
                        case "precipMM":
366
                            {
367
                                double rainMM;
368
                                Double.TryParse(obj.Current.Value.GetString(), out rainMM);
369
                                res.rain = rainMM > 0 ? 100 : 0;
370
                                break;
371
                            }
372
                        case "windspeedKmph":
373
                            {
374
                                double wind;
375
                                Double.TryParse(obj.Current.Value.GetString(), out wind);
376
                                res.wind = wind * 1000 / (60.0 * 60.0);
377
                                break;
378
                            }
379
                    }
380
                }
381

  
382
            }
383

  
384
            return res;
385
        }
386

  
65 387
    }
66 388
}
Server/ServerApp/Parser/Parsers/WeatherParser.cs
110 110
                recordedAmount[1] += list[i].rain;
111 111
                recordedAmount[2] += list[i].wind;
112 112

  
113
                if (LuxToConditions.TransferLuxToConditions(list[i].lum * 1000) != WeatherConditions.Dark)
113
                if (ValueToConditions.TransferLuxToConditions(list[i].lum * 1000) != WeatherConditions.Dark)
114 114
                    recordedAmount[3] += list[i].lum * 1000; weatherValues++;
115 115

  
116 116
                values++;
Server/ServerApp/ServerApp.csproj
151 151
    <Compile Include="Parser\InputData\LogInInstance.cs" />
152 152
    <Compile Include="Parser\InputData\WeatherInstance.cs" />
153 153
    <Compile Include="Parser\OutputInfo\ActivityInfo.cs" />
154
    <Compile Include="Parser\OutputInfo\JisInfo.cs" />
155
    <Compile Include="Parser\OutputInfo\LogInInfo.cs" />
156
    <Compile Include="Parser\OutputInfo\LumInfo.cs" />
157
    <Compile Include="Parser\OutputInfo\RainInfo.cs" />
158
    <Compile Include="Parser\OutputInfo\TempInfo.cs" />
159 154
    <Compile Include="Parser\OutputInfo\WeatherConditions.cs" />
160 155
    <Compile Include="Parser\OutputInfo\WeatherInfo.cs" />
161
    <Compile Include="Parser\OutputInfo\WindInfo.cs" />
162 156
    <Compile Include="Parser\Parsers\DataParser.cs" />
163 157
    <Compile Include="Parser\Parsers\JisParser.cs" />
164 158
    <Compile Include="Parser\Parsers\JsonParser.cs" />

Také k dispozici: Unified diff