2016-10-26 21:02:01 -07:00
using System ;
2021-02-01 12:11:32 -08:00
using System.Collections.Generic ;
using System.Linq ;
2020-08-20 14:36:36 -07:00
2021-02-01 12:11:32 -08:00
using SabreTools.Core ;
using SabreTools.Core.Tools ;
using SabreTools.DatFiles ;
using SabreTools.DatItems ;
2020-12-07 14:29:45 -08:00
using SabreTools.Logging ;
2016-10-26 21:02:01 -07:00
2020-12-08 13:48:57 -08:00
namespace SabreTools.Filtering
2016-10-26 21:02:01 -07:00
{
2019-01-08 17:40:28 -08:00
/// <summary>
/// Represents the filtering operations that need to be performed on a set of items, usually a DAT
/// </summary>
2021-02-01 12:11:32 -08:00
public class Filter
2019-01-08 17:40:28 -08:00
{
2021-02-01 12:11:32 -08:00
#region Fields
/// <summary>
/// Filter for DatItem fields
/// </summary>
2024-02-28 19:19:50 -05:00
public DatItemFilter ? DatItemFilter { get ; set ; }
2021-02-01 12:11:32 -08:00
/// <summary>
/// Filter for Machine fields
/// </summary>
2024-02-28 19:19:50 -05:00
public MachineFilter ? MachineFilter { get ; set ; }
2021-02-01 12:11:32 -08:00
2021-01-15 13:08:10 -08:00
#endregion
2020-10-07 15:42:30 -07:00
#region Logging
/// <summary>
/// Logging object
/// </summary>
2020-12-12 22:16:43 -08:00
protected Logger logger ;
2020-10-07 15:42:30 -07:00
#endregion
2020-10-07 16:37:10 -07:00
#region Constructors
/// <summary>
/// Constructor
/// </summary>
public Filter ( )
{
logger = new Logger ( this ) ;
}
#endregion
2021-02-01 12:11:32 -08:00
#region Population
/// <summary>
/// Populate the filters objects using a set of key:value filters
/// </summary>
/// <param name="filters">List of key:value where ~key/!key is negated</param>
2024-03-05 13:32:49 -05:00
public void PopulateFiltersFromList ( List < string > ? filters )
2021-02-01 12:11:32 -08:00
{
// Instantiate the filters, if necessary
MachineFilter ? ? = new MachineFilter ( ) ;
DatItemFilter ? ? = new DatItemFilter ( ) ;
// If the list is null or empty, just return
if ( filters = = null | | filters . Count = = 0 )
return ;
2023-04-19 16:39:58 -04:00
InternalStopwatch watch = new ( "Populating filters from list" ) ;
2021-02-02 14:20:56 -08:00
2021-02-01 12:11:32 -08:00
foreach ( string filterPair in filters )
{
2024-02-28 19:19:50 -05:00
( string? field , string? value , bool negate ) = ProcessFilterPair ( filterPair ) ;
2021-02-01 12:11:32 -08:00
// If we don't even have a possible filter pair
if ( field = = null & & value = = null )
continue ;
// Machine fields
MachineField machineField = field . AsMachineField ( ) ;
if ( machineField ! = MachineField . NULL )
{
MachineFilter . SetFilter ( machineField , value , negate ) ;
2021-02-03 09:16:48 -08:00
MachineFilter . HasFilters = true ;
2021-02-01 12:11:32 -08:00
continue ;
}
// DatItem fields
DatItemField datItemField = field . AsDatItemField ( ) ;
if ( datItemField ! = DatItemField . NULL )
{
DatItemFilter . SetFilter ( datItemField , value , negate ) ;
2021-02-03 09:16:48 -08:00
DatItemFilter . HasFilters = true ;
2021-02-01 12:11:32 -08:00
continue ;
}
// If we didn't match anything, log an error
logger . Warning ( $"The value {field} did not match any filterable field names. Please check the wiki for more details on supported field names." ) ;
}
2021-02-02 14:20:56 -08:00
watch . Stop ( ) ;
2021-02-01 12:11:32 -08:00
}
2020-07-18 21:35:17 -07:00
2020-12-12 22:03:04 -08:00
/// <summary>
2020-12-12 22:16:43 -08:00
/// Split the parts of a filter statement
2020-12-12 22:03:04 -08:00
/// </summary>
2020-12-12 22:16:43 -08:00
/// <param name="filter">key:value where ~key/!key is negated</param>
2024-02-28 19:19:50 -05:00
protected ( string? field , string? value , bool negate ) ProcessFilterPair ( string filter )
2020-12-12 22:03:04 -08:00
{
2020-12-12 22:16:43 -08:00
// If we don't even have a possible filter pair
2023-04-19 16:39:58 -04:00
if ( ! filter . Contains ( ':' ) )
2020-12-12 22:03:04 -08:00
{
2020-12-12 22:16:43 -08:00
logger . Warning ( $"'{filter}` is not a valid filter string. Valid filter strings are of the form 'key:value'. Please refer to README.1ST or the help feature for more details." ) ;
return ( null , null , false ) ;
2020-12-12 22:03:04 -08:00
}
2020-12-12 22:16:43 -08:00
string filterTrimmed = filter . Trim ( '"' , ' ' , '\t' ) ;
bool negate = filterTrimmed . StartsWith ( "!" )
| | filterTrimmed . StartsWith ( "~" )
| | filterTrimmed . StartsWith ( "not-" ) ;
filterTrimmed = filterTrimmed . TrimStart ( '!' , '~' ) ;
2024-02-28 23:09:31 -05:00
filterTrimmed = filterTrimmed . StartsWith ( "not-" ) ? filterTrimmed . Substring ( 4 ) : filterTrimmed ;
2020-12-12 22:03:04 -08:00
2020-12-12 22:16:43 -08:00
string filterFieldString = filterTrimmed . Split ( ':' ) [ 0 ] . ToLowerInvariant ( ) . Trim ( '"' , ' ' , '\t' ) ;
2024-02-28 23:09:31 -05:00
string filterValue = filterTrimmed . Substring ( filterFieldString . Length + 1 ) . Trim ( '"' , ' ' , '\t' ) ;
2020-12-12 22:16:43 -08:00
return ( filterFieldString , filterValue , negate ) ;
2020-12-12 22:03:04 -08:00
}
2020-09-03 23:27:05 -07:00
/// <summary>
/// Set a bool? filter
/// </summary>
/// <param name="filterItem">FilterItem to populate</param>
/// <param name="value">String value to add</param>
/// <param name="negate">True to set negative filter, false otherwise</param>
2024-02-28 19:19:50 -05:00
protected static void SetBooleanFilter ( FilterItem < bool? > filterItem , string? value , bool negate )
2020-09-03 23:27:05 -07:00
{
2024-02-28 19:19:50 -05:00
if ( negate | | ( value ? . Equals ( "false" , StringComparison . OrdinalIgnoreCase ) ? ? false ) )
2020-09-03 23:27:05 -07:00
filterItem . Neutral = false ;
else
filterItem . Neutral = true ;
}
2020-09-04 11:04:58 -07:00
/// <summary>
/// Set a long? filter
/// </summary>
/// <param name="filterItem">FilterItem to populate</param>
/// <param name="value">String value to add</param>
/// <param name="negate">True to set negative filter, false otherwise</param>
2024-02-28 19:19:50 -05:00
protected static void SetDoubleFilter ( FilterItem < double? > filterItem , string? value , bool negate )
2020-09-04 11:04:58 -07:00
{
bool? operation = null ;
2024-02-28 19:19:50 -05:00
if ( value ? . StartsWith ( ">" ) = = true )
2020-09-04 11:04:58 -07:00
operation = true ;
2024-02-28 19:19:50 -05:00
else if ( value ? . StartsWith ( "<" ) = = true )
2020-09-04 11:04:58 -07:00
operation = false ;
2024-02-28 19:19:50 -05:00
else if ( value ? . StartsWith ( "=" ) = = true )
2020-09-04 11:04:58 -07:00
operation = null ;
2024-02-28 19:19:50 -05:00
string? valueString = value ? . TrimStart ( '>' , '<' , '=' ) ;
2020-09-04 11:04:58 -07:00
if ( ! Double . TryParse ( valueString , out double valueDouble ) )
return ;
// Equal
if ( operation = = null & & ! negate )
{
filterItem . Neutral = valueDouble ;
}
// Not Equal
else if ( operation = = null & & negate )
{
filterItem . Negative = valueDouble - 1 ;
filterItem . Positive = valueDouble + 1 ;
}
// Greater Than or Equal
else if ( operation = = true & & ! negate )
{
filterItem . Positive = valueDouble ;
}
// Strictly Less Than
else if ( operation = = true & & negate )
{
filterItem . Negative = valueDouble - 1 ;
}
// Less Than or Equal
else if ( operation = = false & & ! negate )
{
filterItem . Negative = valueDouble ;
}
// Strictly Greater Than
else if ( operation = = false & & negate )
{
filterItem . Positive = valueDouble + 1 ;
}
}
2020-09-03 23:27:05 -07:00
/// <summary>
/// Set a long? filter
/// </summary>
/// <param name="filterItem">FilterItem to populate</param>
/// <param name="value">String value to add</param>
/// <param name="negate">True to set negative filter, false otherwise</param>
2024-02-28 19:19:50 -05:00
protected static void SetLongFilter ( FilterItem < long? > filterItem , string? value , bool negate )
2020-09-03 23:27:05 -07:00
{
bool? operation = null ;
2024-02-28 19:19:50 -05:00
if ( value ? . StartsWith ( ">" ) = = true )
2020-09-03 23:27:05 -07:00
operation = true ;
2024-02-28 19:19:50 -05:00
else if ( value ? . StartsWith ( "<" ) = = true )
2020-09-03 23:27:05 -07:00
operation = false ;
2024-02-28 19:19:50 -05:00
else if ( value ? . StartsWith ( "=" ) = = true )
2020-09-03 23:27:05 -07:00
operation = null ;
2024-02-28 19:19:50 -05:00
string? valueString = value ? . TrimStart ( '>' , '<' , '=' ) ;
2023-08-11 22:32:44 -04:00
long? valueLong = NumberHelper . ConvertToInt64 ( valueString ) ;
2021-01-15 13:08:10 -08:00
if ( valueLong = = null )
2020-09-03 23:27:05 -07:00
return ;
// Equal
if ( operation = = null & & ! negate )
{
filterItem . Neutral = valueLong ;
}
// Not Equal
else if ( operation = = null & & negate )
{
filterItem . Negative = valueLong - 1 ;
filterItem . Positive = valueLong + 1 ;
}
// Greater Than or Equal
else if ( operation = = true & & ! negate )
{
filterItem . Positive = valueLong ;
}
// Strictly Less Than
else if ( operation = = true & & negate )
{
filterItem . Negative = valueLong - 1 ;
}
// Less Than or Equal
else if ( operation = = false & & ! negate )
{
filterItem . Negative = valueLong ;
}
// Strictly Greater Than
else if ( operation = = false & & negate )
{
filterItem . Positive = valueLong + 1 ;
}
}
/// <summary>
/// Set a string filter
/// </summary>
/// <param name="filterItem">FilterItem to populate</param>
/// <param name="value">String value to add</param>
/// <param name="negate">True to set negative filter, false otherwise</param>
2024-02-28 19:19:50 -05:00
protected static void SetStringFilter ( FilterItem < string > filterItem , string? value , bool negate )
2020-09-03 23:27:05 -07:00
{
if ( negate )
filterItem . NegativeSet . Add ( value ) ;
else
filterItem . PositiveSet . Add ( value ) ;
}
2020-07-18 21:35:17 -07:00
#endregion
2021-02-01 12:11:32 -08:00
#region Running
/// <summary>
/// Apply a set of Filters on the DatFile
/// </summary>
/// <param name="datFile">Current DatFile object to run operations on</param>
/// <param name="perMachine">True if entire machines are considered, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>True if the DatFile was filtered, false on error</returns>
public bool ApplyFilters ( DatFile datFile , bool perMachine = false , bool throwOnError = false )
{
// If we have null filters, return false
if ( MachineFilter = = null | | DatItemFilter = = null )
return false ;
2021-02-03 09:08:59 -08:00
// If no filters were set, return true
2021-02-03 09:16:48 -08:00
if ( ! MachineFilter . HasFilters & & ! DatItemFilter . HasFilters )
2021-02-03 09:08:59 -08:00
return true ;
2023-04-19 16:39:58 -04:00
InternalStopwatch watch = new ( "Applying filters to DAT" ) ;
2021-02-02 14:20:56 -08:00
2021-02-01 12:11:32 -08:00
// If we're filtering per machine, bucket by machine first
if ( perMachine )
datFile . Items . BucketBy ( ItemKey . Machine , DedupeType . None ) ;
try
{
// Loop over every key in the dictionary
2024-02-28 19:19:50 -05:00
List < string > keys = [ . . datFile . Items . Keys ] ;
2021-02-01 12:11:32 -08:00
foreach ( string key in keys )
{
// For every item in the current key
bool machinePass = true ;
2024-02-28 19:19:50 -05:00
var items = datFile . Items [ key ] ;
if ( items = = null )
continue ;
2021-02-01 12:11:32 -08:00
foreach ( DatItem item in items )
{
// If we have a null item, we can't pass it
if ( item = = null )
continue ;
// If the item is already filtered out, we skip
if ( item . Remove )
continue ;
// If the rom doesn't pass the filter, mark for removal
2021-02-01 12:35:59 -08:00
if ( ! PassesAllFilters ( item ) )
2021-02-01 12:11:32 -08:00
{
item . Remove = true ;
// If we're in machine mode, set and break
if ( perMachine )
{
machinePass = false ;
break ;
}
}
}
// If we didn't pass and we're in machine mode, set all items as remove
if ( perMachine & & ! machinePass )
{
foreach ( DatItem item in items )
{
item . Remove = true ;
}
}
// Assign back for caution
datFile . Items [ key ] = items ;
}
}
catch ( Exception ex ) when ( ! throwOnError )
{
logger . Error ( ex ) ;
return false ;
}
2021-02-02 14:20:56 -08:00
finally
{
watch . Stop ( ) ;
}
2021-02-01 12:11:32 -08:00
return true ;
}
/// <summary>
/// Check to see if a DatItem passes the filters
/// </summary>
/// <param name="datItem">DatItem to check</param>
/// <returns>True if the item passed the filter, false otherwise</returns>
2021-02-01 12:35:59 -08:00
internal bool PassesAllFilters ( DatItem datItem )
2021-02-01 12:11:32 -08:00
{
// Null item means it will never pass
if ( datItem = = null )
return false ;
// Filter on Machine fields
2024-02-28 19:19:50 -05:00
if ( MachineFilter = = null | | ( MachineFilter . HasFilters & & ! MachineFilter . PassesFilters ( datItem . Machine ) ) )
2021-02-01 12:11:32 -08:00
return false ;
2021-02-03 09:16:48 -08:00
// If we have no DatItemFilters set, just return true
2024-02-28 19:19:50 -05:00
if ( DatItemFilter = = null | | ! DatItemFilter . HasFilters )
2021-02-03 09:16:48 -08:00
return true ;
2021-02-01 12:11:32 -08:00
// Filter on DatItem fields
return DatItemFilter . PassesFilters ( datItem ) ;
}
2020-09-08 12:54:41 -07:00
/// <summary>
/// Determines if a value passes a bool? filter
/// </summary>
/// <param name="filterItem">Filter item to check</param>
/// <param name="value">Value to check</param>
/// <returns>True if the value passes, false otherwise</returns>
2021-01-29 13:38:47 -08:00
protected static bool PassBoolFilter ( FilterItem < bool? > filterItem , bool? value )
2020-09-08 12:54:41 -07:00
{
if ( filterItem . MatchesNeutral ( null , value ) = = false )
return false ;
return true ;
}
/// <summary>
/// Determines if a value passes a double? filter
/// </summary>
/// <param name="filterItem">Filter item to check</param>
/// <param name="value">Value to check</param>
/// <returns>True if the value passes, false otherwise</returns>
2021-01-29 13:38:47 -08:00
protected static bool PassDoubleFilter ( FilterItem < double? > filterItem , double? value )
2020-09-08 12:54:41 -07:00
{
if ( filterItem . MatchesNeutral ( null , value ) = = false )
return false ;
else if ( filterItem . MatchesPositive ( null , value ) = = false )
return false ;
else if ( filterItem . MatchesNegative ( null , value ) = = false )
return false ;
return true ;
}
/// <summary>
/// Determines if a value passes a long? filter
/// </summary>
/// <param name="filterItem">Filter item to check</param>
/// <param name="value">Value to check</param>
/// <returns>True if the value passes, false otherwise</returns>
2021-01-29 13:38:47 -08:00
protected static bool PassLongFilter ( FilterItem < long? > filterItem , long? value )
2020-09-08 12:54:41 -07:00
{
if ( filterItem . MatchesNeutral ( null , value ) = = false )
return false ;
else if ( filterItem . MatchesPositive ( null , value ) = = false )
return false ;
else if ( filterItem . MatchesNegative ( null , value ) = = false )
return false ;
return true ;
}
/// <summary>
/// Determines if a value passes a string filter
/// </summary>
/// <param name="filterItem">Filter item to check</param>
/// <param name="value">Value to check</param>
/// <returns>True if the value passes, false otherwise</returns>
2024-02-28 19:19:50 -05:00
protected static bool PassStringFilter ( FilterItem < string > filterItem , string? value )
2020-09-08 12:54:41 -07:00
{
if ( filterItem . MatchesPositiveSet ( value ) = = false )
return false ;
if ( filterItem . MatchesNegativeSet ( value ) = = true )
return false ;
return true ;
}
#endregion
2019-01-08 17:40:28 -08:00
}
2016-10-26 21:02:01 -07:00
}