OSDN Git Service

C#7.0で追加されたExpression-Bodiedメンバの構文を使用する
[opentween/open-tween.git] / OpenTween / Models / PostFilterRule.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2013 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
4 // 
5 // This file is part of OpenTween.
6 // 
7 // This program is free software; you can redistribute it and/or modify it
8 // under the terms of the GNU General Public License as published by the Free
9 // Software Foundation; either version 3 of the License, or (at your option)
10 // any later version.
11 // 
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15 // for more details. 
16 // 
17 // You should have received a copy of the GNU General Public License along
18 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
19 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
20 // Boston, MA 02110-1301, USA.
21
22 using System;
23 using System.Collections.Generic;
24 using System.ComponentModel;
25 using System.Linq;
26 using System.Linq.Expressions;
27 using System.Runtime.CompilerServices;
28 using System.Text;
29 using System.Text.RegularExpressions;
30 using System.Xml.Serialization;
31
32 namespace OpenTween.Models
33 {
34     /// <summary>
35     /// タブで使用する振り分けルールを表すクラス
36     /// </summary>
37     [XmlType("FiltersClass")]
38     public class PostFilterRule : NotifyPropertyChangedBase, IEquatable<PostFilterRule>
39     {
40         /// <summary>
41         /// Compile() メソッドの呼び出しが必要な状態か否か
42         /// </summary>
43         [XmlIgnore]
44         public bool IsDirty { get; protected set; }
45
46         /// <summary>
47         /// コンパイルされた振り分けルール
48         /// </summary>
49         [XmlIgnore]
50         protected Func<PostClass, MyCommon.HITRESULT> FilterDelegate;
51
52         /// <summary>
53         /// 振り分けルールの概要
54         /// </summary>
55         [XmlIgnore]
56         public string SummaryText
57         {
58             get { return this.MakeSummary(); }
59         }
60
61         /// <summary>
62         /// ExecFilter() メソッドの実行時に自動でコンパイルを実行する
63         /// </summary>
64         /// <remarks>
65         /// テスト用途以外では AutoCompile に頼らず事前に Compile() メソッドを呼び出すこと
66         /// </remarks>
67         internal static bool AutoCompile { get; set; }
68
69         public bool Enabled
70         {
71             get => this._enabled;
72             set => this.SetProperty(ref this._enabled, value);
73         }
74         private bool _enabled;
75
76         [XmlElement("NameFilter")]
77         public string FilterName
78         {
79             get => this._FilterName;
80             set => this.SetProperty(ref this._FilterName, value);
81         }
82         private string _FilterName;
83
84         [XmlElement("ExNameFilter")]
85         public string ExFilterName
86         {
87             get => this._ExFilterName;
88             set => this.SetProperty(ref this._ExFilterName, value);
89         }
90         private string _ExFilterName;
91
92         [XmlArray("BodyFilterArray")]
93         public string[] FilterBody
94         {
95             get => this._FilterBody;
96             set => this.SetProperty(ref this._FilterBody, value ?? throw new ArgumentNullException(nameof(value)));
97         }
98         private string[] _FilterBody = new string[0];
99
100         [XmlArray("ExBodyFilterArray")]
101         public string[] ExFilterBody
102         {
103             get => this._ExFilterBody;
104             set => this.SetProperty(ref this._ExFilterBody, value ?? throw new ArgumentNullException(nameof(value)));
105         }
106         private string[] _ExFilterBody = new string[0];
107
108         [XmlElement("SearchBoth")]
109         public bool UseNameField
110         {
111             get => this._UseNameField;
112             set => this.SetProperty(ref this._UseNameField, value);
113         }
114         private bool _UseNameField;
115
116         [XmlElement("ExSearchBoth")]
117         public bool ExUseNameField
118         {
119             get => this._ExUseNameField;
120             set => this.SetProperty(ref this._ExUseNameField, value);
121         }
122         private bool _ExUseNameField;
123
124         [XmlElement("MoveFrom")]
125         public bool MoveMatches
126         {
127             get => this._MoveMatches;
128             set => this.SetProperty(ref this._MoveMatches, value);
129         }
130         private bool _MoveMatches;
131
132         [XmlElement("SetMark")]
133         public bool MarkMatches
134         {
135             get => this._MarkMatches;
136             set => this.SetProperty(ref this._MarkMatches, value);
137         }
138         private bool _MarkMatches;
139
140         [XmlElement("SearchUrl")]
141         public bool FilterByUrl
142         {
143             get => this._FilterByUrl;
144             set => this.SetProperty(ref this._FilterByUrl, value);
145         }
146         private bool _FilterByUrl;
147
148         [XmlElement("ExSearchUrl")]
149         public bool ExFilterByUrl
150         {
151             get => this._ExFilterByUrl;
152             set => this.SetProperty(ref this._ExFilterByUrl, value);
153         }
154         private bool _ExFilterByUrl;
155
156         public bool CaseSensitive
157         {
158             get => this._CaseSensitive;
159             set => this.SetProperty(ref this._CaseSensitive, value);
160         }
161         private bool _CaseSensitive;
162
163         public bool ExCaseSensitive
164         {
165             get => this._ExCaseSensitive;
166             set => this.SetProperty(ref this._ExCaseSensitive, value);
167         }
168         private bool _ExCaseSensitive;
169
170         public bool UseLambda
171         {
172             get => this._UseLambda;
173             set => this.SetProperty(ref this._UseLambda, value);
174         }
175         private bool _UseLambda;
176
177         public bool ExUseLambda
178         {
179             get => this._ExUseLambda;
180             set => this.SetProperty(ref this._ExUseLambda, value);
181         }
182         private bool _ExUseLambda;
183
184         public bool UseRegex
185         {
186             get => this._UseRegex;
187             set => this.SetProperty(ref this._UseRegex, value);
188         }
189         private bool _UseRegex;
190
191         public bool ExUseRegex
192         {
193             get => this._ExUseRegex;
194             set => this.SetProperty(ref this._ExUseRegex, value);
195         }
196         private bool _ExUseRegex;
197
198         [XmlElement("IsRt")]
199         public bool FilterRt
200         {
201             get => this._FilterRt;
202             set => this.SetProperty(ref this._FilterRt, value);
203         }
204         private bool _FilterRt;
205
206         [XmlElement("IsExRt")]
207         public bool ExFilterRt
208         {
209             get => this._ExFilterRt;
210             set => this.SetProperty(ref this._ExFilterRt, value);
211         }
212         private bool _ExFilterRt;
213
214         [XmlElement("Source")]
215         public string FilterSource
216         {
217             get => this._FilterSource;
218             set => this.SetProperty(ref this._FilterSource, value);
219         }
220         private string _FilterSource;
221
222         [XmlElement("ExSource")]
223         public string ExFilterSource
224         {
225             get => this._ExFilterSource;
226             set => this.SetProperty(ref this._ExFilterSource, value);
227         }
228         private string _ExFilterSource;
229
230         public PostFilterRule()
231         {
232             this.IsDirty = true;
233
234             this.Enabled = true;
235             this.MarkMatches = true;
236             this.UseNameField = true;
237             this.ExUseNameField = true;
238         }
239
240         static PostFilterRule()
241         {
242             // TODO: TabsClass とかの改修が終わるまでデフォルト有効
243             PostFilterRule.AutoCompile = true;
244         }
245
246         /// <summary>
247         /// 振り分けルールをコンパイルします
248         /// </summary>
249         public void Compile()
250         {
251             if (!this.Enabled)
252             {
253                 this.FilterDelegate = x => MyCommon.HITRESULT.None;
254                 this.IsDirty = false;
255                 return;
256             }
257
258             var postParam = Expression.Parameter(typeof(PostClass), "x");
259
260             var matchExpr = this.MakeFiltersExpr(
261                 postParam,
262                 this.FilterName, this.FilterBody, this.FilterSource, this.FilterRt,
263                 this.UseRegex, this.CaseSensitive, this.UseNameField, this.UseLambda, this.FilterByUrl);
264
265             var excludeExpr = this.MakeFiltersExpr(
266                 postParam,
267                 this.ExFilterName, this.ExFilterBody, this.ExFilterSource, this.ExFilterRt,
268                 this.ExUseRegex, this.ExCaseSensitive, this.ExUseNameField, this.ExUseLambda, this.ExFilterByUrl);
269
270             Expression<Func<PostClass, MyCommon.HITRESULT>> filterExpr;
271
272             if (matchExpr != null)
273             {
274                 MyCommon.HITRESULT hitResult;
275
276                 if (this.MoveMatches)
277                     hitResult = MyCommon.HITRESULT.Move;
278                 else if (this.MarkMatches)
279                     hitResult = MyCommon.HITRESULT.CopyAndMark;
280                 else
281                     hitResult = MyCommon.HITRESULT.Copy;
282
283                 if (excludeExpr != null)
284                 {
285                     // x => matchExpr ? (!excludeExpr ? hitResult : None) : None
286                     filterExpr =
287                         Expression.Lambda<Func<PostClass, MyCommon.HITRESULT>>(
288                             Expression.Condition(
289                                 matchExpr,
290                                 Expression.Condition(
291                                     Expression.Not(excludeExpr),
292                                     Expression.Constant(hitResult),
293                                     Expression.Constant(MyCommon.HITRESULT.None)),
294                                 Expression.Constant(MyCommon.HITRESULT.None)),
295                             postParam);
296                 }
297                 else
298                 {
299                     // x => matchExpr ? hitResult : None
300                     filterExpr =
301                         Expression.Lambda<Func<PostClass, MyCommon.HITRESULT>>(
302                             Expression.Condition(
303                                 matchExpr,
304                                 Expression.Constant(hitResult),
305                                 Expression.Constant(MyCommon.HITRESULT.None)),
306                             postParam);
307                 }
308             }
309             else if (excludeExpr != null)
310             {
311                 // x => excludeExpr ? Exclude : None
312                 filterExpr =
313                     Expression.Lambda<Func<PostClass, MyCommon.HITRESULT>>(
314                         Expression.Condition(
315                             excludeExpr,
316                             Expression.Constant(MyCommon.HITRESULT.Exclude),
317                             Expression.Constant(MyCommon.HITRESULT.None)),
318                         postParam);
319             }
320             else // matchExpr == null && excludeExpr == null
321             {
322                 filterExpr = x => MyCommon.HITRESULT.None;
323             }
324
325             this.FilterDelegate = filterExpr.Compile();
326             this.IsDirty = false;
327         }
328
329         protected virtual Expression MakeFiltersExpr(
330             ParameterExpression postParam,
331             string filterName, string[] filterBody, string filterSource, bool filterRt,
332             bool useRegex, bool caseSensitive, bool useNameField, bool useLambda, bool filterByUrl)
333         {
334             var filterExprs = new List<Expression>();
335
336             if (useNameField && !string.IsNullOrEmpty(filterName))
337             {
338                 filterExprs.Add(Expression.OrElse(
339                     this.MakeGenericFilter(postParam, "ScreenName", filterName, useRegex, caseSensitive, exactMatch: true),
340                     this.MakeGenericFilter(postParam, "RetweetedBy", filterName, useRegex, caseSensitive, exactMatch: true)));
341             }
342             foreach (var body in filterBody)
343             {
344                 if (string.IsNullOrEmpty(body))
345                     continue;
346
347                 Expression bodyExpr;
348                 if (useLambda)
349                 {
350                     // TODO DynamicQuery相当のGPLv3互換なライブラリで置換する
351                     Expression<Func<PostClass, bool>> lambdaExpr = x => false;
352                     bodyExpr = lambdaExpr.Body;
353                 }
354                 else
355                 {
356                     if (filterByUrl)
357                         bodyExpr = this.MakeGenericFilter(postParam, "Text", body, useRegex, caseSensitive);
358                     else
359                         bodyExpr = this.MakeGenericFilter(postParam, "TextFromApi", body, useRegex, caseSensitive);
360
361                     // useNameField = false の場合は ScreenName と RetweetedBy も filterBody のマッチ対象となる
362                     if (!useNameField)
363                     {
364                         bodyExpr = Expression.OrElse(
365                             bodyExpr,
366                             this.MakeGenericFilter(postParam, "ScreenName", body, useRegex, caseSensitive, exactMatch: true));
367
368                         // bodyExpr || x.RetweetedBy != null && <MakeGenericFilter()>
369                         bodyExpr = Expression.OrElse(
370                             bodyExpr,
371                             Expression.AndAlso(
372                                 Expression.NotEqual(
373                                     Expression.Property(
374                                         postParam,
375                                         typeof(PostClass).GetProperty("RetweetedBy")),
376                                     Expression.Constant(null)),
377                                 this.MakeGenericFilter(postParam, "RetweetedBy", body, useRegex, caseSensitive, exactMatch: true)));
378                     }
379                 }
380
381                 filterExprs.Add(bodyExpr);
382             }
383             if (!string.IsNullOrEmpty(filterSource))
384             {
385                 if (filterByUrl)
386                     filterExprs.Add(this.MakeGenericFilter(postParam, "SourceHtml", filterSource, useRegex, caseSensitive));
387                 else
388                     filterExprs.Add(this.MakeGenericFilter(postParam, "Source", filterSource, useRegex, caseSensitive, exactMatch: true));
389             }
390             if (filterRt)
391             {
392                 // x.RetweetedId != null
393                 filterExprs.Add(Expression.NotEqual(
394                     Expression.Property(
395                         postParam,
396                         typeof(PostClass).GetProperty("RetweetedId")),
397                     Expression.Constant(null)));
398             }
399
400             if (filterExprs.Count == 0)
401             {
402                 return null;
403             }
404             else
405             {
406                 // filterExpr[0] && filterExpr[1] && ...
407                 var filterExpr = filterExprs[0];
408                 foreach (var expr in filterExprs.Skip(1))
409                 {
410                     filterExpr = Expression.AndAlso(filterExpr, expr);
411                 }
412
413                 return filterExpr;
414             }
415         }
416
417         protected Expression MakeGenericFilter(
418             ParameterExpression postParam, string targetFieldName, string pattern,
419             bool useRegex, bool caseSensitive, bool exactMatch = false)
420         {
421             // x.<targetFieldName>
422             var targetField = Expression.Property(
423                 postParam,
424                 typeof(PostClass).GetProperty(targetFieldName));
425
426             // targetField ?? ""
427             var targetValue = Expression.Coalesce(targetField, Expression.Constant(string.Empty));
428
429             if (useRegex)
430             {
431                 var regex = new Regex(pattern, caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase);
432
433                 // regex.IsMatch(targetField)
434                 return Expression.Call(
435                     Expression.Constant(regex),
436                     typeof(Regex).GetMethod("IsMatch", new[] { typeof(string) }),
437                     targetValue);
438             }
439             else
440             {
441                 var compOpt = caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
442
443                 if (exactMatch)
444                 {
445                     // 完全一致
446                     // pattern.Equals(targetField, compOpt)
447                     return Expression.Call(
448                         Expression.Constant(pattern),
449                         typeof(string).GetMethod("Equals", new[] { typeof(string), typeof(StringComparison) }),
450                         targetValue,
451                         Expression.Constant(compOpt));
452
453                 }
454                 else
455                 {
456                     // 部分一致
457                     // targetField.IndexOf(pattern, compOpt) != -1
458                     return Expression.NotEqual(
459                         Expression.Call(
460                             targetValue,
461                             typeof(string).GetMethod("IndexOf", new[] { typeof(string), typeof(StringComparison) }),
462                             Expression.Constant(pattern),
463                             Expression.Constant(compOpt)),
464                         Expression.Constant(-1));
465                 }
466             }
467         }
468
469         /// <summary>
470         /// ツイートの振り分けを実行する
471         /// </summary>
472         /// <param name="post">振り分けるツイート</param>
473         /// <returns>振り分け結果</returns>
474         public MyCommon.HITRESULT ExecFilter(PostClass post)
475         {
476             if (this.IsDirty)
477             {
478                 if (!PostFilterRule.AutoCompile)
479                     throw new InvalidOperationException("振り分け実行前に Compile() を呼び出す必要があります");
480
481                 this.Compile();
482             }
483
484             return this.FilterDelegate(post);
485         }
486
487         public PostFilterRule Clone()
488         {
489             return new PostFilterRule
490             {
491                 FilterBody = this.FilterBody,
492                 FilterName = this.FilterName,
493                 FilterSource = this.FilterSource,
494                 FilterRt = this.FilterRt,
495                 FilterByUrl = this.FilterByUrl,
496                 UseLambda = this.UseLambda,
497                 UseNameField = this.UseNameField,
498                 UseRegex = this.UseRegex,
499                 CaseSensitive = this.CaseSensitive,
500                 ExFilterBody = this.ExFilterBody,
501                 ExFilterName = this.ExFilterName,
502                 ExFilterSource = this.ExFilterSource,
503                 ExFilterRt = this.ExFilterRt,
504                 ExFilterByUrl = this.ExFilterByUrl,
505                 ExUseLambda = this.ExUseLambda,
506                 ExUseNameField = this.ExUseNameField,
507                 ExUseRegex = this.ExUseRegex,
508                 ExCaseSensitive = this.ExCaseSensitive,
509                 MoveMatches = this.MoveMatches,
510                 MarkMatches = this.MarkMatches,
511             };
512         }
513
514         /// <summary>
515         /// 振り分けルールの文字列表現を返します
516         /// </summary>
517         /// <returns>振り分けルールの概要</returns>
518         public override string ToString()
519         {
520             return this.SummaryText;
521         }
522
523         protected override void OnPropertyChanged(PropertyChangedEventArgs e)
524         {
525             this.IsDirty = true;
526             base.OnPropertyChanged(e);
527         }
528
529         #region from Tween v1.1.0.0
530
531         // The code in this region block is based on code written by the following authors:
532         //   (C) 2007 kiri_feather (@kiri_feather) <kiri.feather@gmail.com>
533         //   (C) 2008 Moz (@syo68k)
534
535         /// <summary>
536         /// フィルタ一覧に表示する文言生成
537         /// </summary>
538         protected virtual string MakeSummary()
539         {
540             var fs = new StringBuilder();
541             if (!this.Enabled)
542             {
543                 fs.Append("<");
544                 fs.Append(Properties.Resources.Disabled);
545                 fs.Append("> ");
546             }
547             if (this.HasMatchConditions())
548             {
549                 if (this.UseNameField)
550                 {
551                     if (!string.IsNullOrEmpty(this.FilterName))
552                     {
553                         fs.AppendFormat(Properties.Resources.SetFiltersText1, this.FilterName);
554                     }
555                     else
556                     {
557                         fs.Append(Properties.Resources.SetFiltersText2);
558                     }
559                 }
560                 if (this.FilterBody.Length > 0)
561                 {
562                     fs.Append(Properties.Resources.SetFiltersText3);
563                     foreach (var bf in this.FilterBody)
564                     {
565                         fs.Append(bf);
566                         fs.Append(" ");
567                     }
568                     fs.Length--;
569                     fs.Append(Properties.Resources.SetFiltersText4);
570                 }
571                 fs.Append("(");
572                 if (this.UseNameField)
573                 {
574                     fs.Append(Properties.Resources.SetFiltersText5);
575                 }
576                 else
577                 {
578                     fs.Append(Properties.Resources.SetFiltersText6);
579                 }
580                 if (this.UseRegex)
581                 {
582                     fs.Append(Properties.Resources.SetFiltersText7);
583                 }
584                 if (this.FilterByUrl)
585                 {
586                     fs.Append(Properties.Resources.SetFiltersText8);
587                 }
588                 if (this.CaseSensitive)
589                 {
590                     fs.Append(Properties.Resources.SetFiltersText13);
591                 }
592                 if (this.FilterRt)
593                 {
594                     fs.Append("RT/");
595                 }
596                 if (this.UseLambda)
597                 {
598                     fs.Append("LambdaExp/");
599                 }
600                 if (!string.IsNullOrEmpty(this.FilterSource))
601                 {
602                     fs.AppendFormat("Src…{0}/", this.FilterSource);
603                 }
604                 fs.Length--;
605                 fs.Append(")");
606             }
607             if (this.HasExcludeConditions())
608             {
609                 //除外
610                 fs.Append(Properties.Resources.SetFiltersText12);
611                 if (this.ExUseNameField)
612                 {
613                     if (!string.IsNullOrEmpty(this.ExFilterName))
614                     {
615                         fs.AppendFormat(Properties.Resources.SetFiltersText1, this.ExFilterName);
616                     }
617                     else
618                     {
619                         fs.Append(Properties.Resources.SetFiltersText2);
620                     }
621                 }
622                 if (this.ExFilterBody.Length > 0)
623                 {
624                     fs.Append(Properties.Resources.SetFiltersText3);
625                     foreach (var bf in this.ExFilterBody)
626                     {
627                         fs.Append(bf);
628                         fs.Append(" ");
629                     }
630                     fs.Length--;
631                     fs.Append(Properties.Resources.SetFiltersText4);
632                 }
633                 fs.Append("(");
634                 if (this.ExUseNameField)
635                 {
636                     fs.Append(Properties.Resources.SetFiltersText5);
637                 }
638                 else
639                 {
640                     fs.Append(Properties.Resources.SetFiltersText6);
641                 }
642                 if (this.ExUseRegex)
643                 {
644                     fs.Append(Properties.Resources.SetFiltersText7);
645                 }
646                 if (this.ExFilterByUrl)
647                 {
648                     fs.Append(Properties.Resources.SetFiltersText8);
649                 }
650                 if (this.ExCaseSensitive)
651                 {
652                     fs.Append(Properties.Resources.SetFiltersText13);
653                 }
654                 if (this.ExFilterRt)
655                 {
656                     fs.Append("RT/");
657                 }
658                 if (this.ExUseLambda)
659                 {
660                     fs.Append("LambdaExp/");
661                 }
662                 if (!string.IsNullOrEmpty(this.ExFilterSource))
663                 {
664                     fs.AppendFormat("Src…{0}/", this.ExFilterSource);
665                 }
666                 fs.Length--;
667                 fs.Append(")");
668             }
669
670             fs.Append("(");
671             if (this.MoveMatches)
672             {
673                 fs.Append(Properties.Resources.SetFiltersText9);
674             }
675             else
676             {
677                 fs.Append(Properties.Resources.SetFiltersText11);
678             }
679             if (!this.MoveMatches && this.MarkMatches)
680             {
681                 fs.Append(Properties.Resources.SetFiltersText10);
682             }
683             else if (!this.MoveMatches)
684             {
685                 fs.Length--;
686             }
687
688             fs.Append(")");
689
690             return fs.ToString();
691         }
692         #endregion
693
694         /// <summary>
695         /// この振り分けルールにマッチ条件が含まれているかを返します
696         /// </summary>
697         public bool HasMatchConditions()
698         {
699             return !string.IsNullOrEmpty(this.FilterName) ||
700                 this.FilterBody.Any(x => !string.IsNullOrEmpty(x)) ||
701                 !string.IsNullOrEmpty(this.FilterSource) ||
702                 this.FilterRt;
703         }
704
705         /// <summary>
706         /// この振り分けルールに除外条件が含まれているかを返します
707         /// </summary>
708         public bool HasExcludeConditions()
709         {
710             return !string.IsNullOrEmpty(this.ExFilterName) ||
711                 this.ExFilterBody.Any(x => !string.IsNullOrEmpty(x)) ||
712                 !string.IsNullOrEmpty(this.ExFilterSource) ||
713                 this.ExFilterRt;
714         }
715
716         public override bool Equals(object obj)
717         {
718             return this.Equals(obj as PostFilterRule);
719         }
720
721         public bool Equals(PostFilterRule other)
722         {
723             if (other == null)
724                 return false;
725
726             if (other.HasMatchConditions() || this.HasMatchConditions())
727             {
728                 if (other.FilterName != this.FilterName ||
729                     !other.FilterBody.SequenceEqual(this.FilterBody) ||
730                     other.FilterSource != this.FilterSource ||
731                     other.FilterRt != this.FilterRt ||
732                     other.FilterByUrl != this.FilterByUrl ||
733                     other.CaseSensitive != this.CaseSensitive ||
734                     other.UseNameField != this.UseNameField ||
735                     other.UseLambda != this.UseLambda ||
736                     other.UseRegex != this.UseRegex)
737                 {
738                     return false;
739                 }
740             }
741
742             if (other.HasExcludeConditions() || this.HasExcludeConditions())
743             {
744                 if (other.ExFilterName != this.ExFilterName ||
745                     !other.ExFilterBody.SequenceEqual(this.ExFilterBody) ||
746                     other.ExFilterSource != this.ExFilterSource ||
747                     other.ExFilterRt != this.ExFilterRt ||
748                     other.ExFilterByUrl != this.ExFilterByUrl ||
749                     other.ExCaseSensitive != this.ExCaseSensitive ||
750                     other.ExUseNameField != this.ExUseNameField ||
751                     other.ExUseLambda != this.ExUseLambda ||
752                     other.ExUseRegex != this.ExUseRegex)
753                 {
754                     return false;
755                 }
756             }
757
758             return true;
759         }
760
761         public override int GetHashCode()
762         {
763             return this.FilterName?.GetHashCode() ?? 0 ^
764                 this.FilterSource?.GetHashCode() ?? 0 ^
765                 this.FilterBody.Select(x => x?.GetHashCode() ?? 0).Sum() ^
766                 this.ExFilterName?.GetHashCode() ?? 0 ^
767                 this.ExFilterSource?.GetHashCode() ?? 0 ^
768                 this.ExFilterBody.Select(x => x?.GetHashCode() ?? 0).Sum();
769         }
770     }
771 }