diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dcb562ca..42daa73c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -118,6 +118,11 @@ android:launchMode="singleTop" android:screenOrientation="portrait" /> + + diff --git a/app/src/main/java/net/pokeranalytics/android/model/Criteria.kt b/app/src/main/java/net/pokeranalytics/android/model/Criteria.kt index 4066ec1a..01f9c3d5 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/Criteria.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/Criteria.kt @@ -14,231 +14,230 @@ import net.pokeranalytics.android.model.Criteria.TournamentFeatures.comparison import net.pokeranalytics.android.model.Criteria.TournamentFees.comparison import net.pokeranalytics.android.model.Criteria.TournamentNames.comparison import net.pokeranalytics.android.model.Criteria.TournamentTypes.comparison +import net.pokeranalytics.android.model.Criteria.TransactionTypes.comparison import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.model.realm.* +import net.pokeranalytics.android.ui.view.RowRepresentable fun List.combined(): List { - val comparatorList = ArrayList>() - this.forEach { criteria -> - comparatorList.add(criteria.queries) - } - return getCombinations(comparatorList) + val comparatorList = ArrayList>() + this.forEach { criteria -> + comparatorList.add(criteria.queries) + } + return getCombinations(comparatorList) } fun getCombinations(queries: List>): List { - if (queries.size == 0) { return listOf() } + if (queries.size == 0) { + return listOf() + } - val mutableQueries = queries.toMutableList() - var combinations = mutableQueries.removeAt(0) + val mutableQueries = queries.toMutableList() + var combinations = mutableQueries.removeAt(0) - for (queryList in mutableQueries) { + for (queryList in mutableQueries) { - val newCombinations = mutableListOf() - combinations.forEach { combinedQuery -> - queryList.forEach { queryToAdd -> - val nq = Query().merge(combinedQuery).merge(queryToAdd) - newCombinations.add(nq) - } - } - combinations = newCombinations - } + val newCombinations = mutableListOf() + combinations.forEach { combinedQuery -> + queryList.forEach { queryToAdd -> + val nq = Query().merge(combinedQuery).merge(queryToAdd) + newCombinations.add(nq) + } + } + combinations = newCombinations + } - return combinations + return combinations } -//fun getCombinations(lists: List): List { -// var combinations: LinkedHashSet = LinkedHashSet() -// var newCombinations: LinkedHashSet -// -// var index = 0 -// -// // extract each of the integers in the first list -// // and add each to ints as a new list -// if (lists.isNotEmpty()) { -// for (i in lists[0]) { -// val newList = ArrayList() -// newList.add(i) -// combinations.add(newList) -// } -// index++ -// } -// while (index < lists.size) { -// val nextList = lists[index] -// newCombinations = LinkedHashSet() -// for (first in combinations) { -// for (second in nextList) { -// val newList = ArrayList() -// newList.addAll(first) -// newList.add(second) -// newCombinations.add(newList) -// } -// } -// combinations = newCombinations -// -// index++ -// } -// -// return combinations.toList() -//} - -sealed class Criteria { - abstract class RealmCriteria : Criteria() { - inline fun comparison(): List { - return compare, T>() - } - } - - abstract class SimpleCriteria(private val conditions:List): Criteria() { - fun comparison(): List { - return conditions.map { Query(it) } - } - } - - abstract class ListCriteria : Criteria() { - inline fun , reified S : Comparable> comparison(): List { - QueryCondition.distinct()?.let { - val values = it.mapNotNull { session -> - when (this) { - is Limits -> if (session.limit is S) { session.limit as S } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue - is TournamentTypes -> if (session.tournamentType is S) { session.tournamentType as S } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue - is TableSizes -> if (session.tableSize is S) { session.tableSize as S } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue - is TournamentFees -> if (session.tournamentEntryFee is S) { session.tournamentEntryFee as S } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue - is Blinds -> if (session.blinds is S) { session.blinds as S } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue - else -> null - } - }.distinct() - return compareList(values = values) - } - return listOf() - } - } - - - object Bankrolls: RealmCriteria() - object Games: RealmCriteria() - object TournamentNames: RealmCriteria() - object Locations: RealmCriteria() - object TournamentFeatures: RealmCriteria() - object TransactionTypes: RealmCriteria() - object Limits: ListCriteria() - object TableSizes: ListCriteria() - object TournamentTypes: ListCriteria() - object MonthsOfYear: SimpleCriteria(List(12) { index -> QueryCondition.AnyMonthOfYear().apply { listOfValues = arrayListOf(index)} }) - object DaysOfWeek: SimpleCriteria(List(7) { index -> QueryCondition.AnyDayOfWeek().apply { listOfValues = arrayListOf(index + 1) } }) - object SessionTypes: SimpleCriteria(listOf(QueryCondition.IsCash, QueryCondition.IsTournament)) - object BankrollTypes: SimpleCriteria(listOf(QueryCondition.IsLive, QueryCondition.IsOnline)) - object DayPeriods: SimpleCriteria(listOf(QueryCondition.IsWeekDay, QueryCondition.IsWeekEnd)) - object Years: ListCriteria() - object AllMonthsUpToNow: ListCriteria() - object Blinds: ListCriteria() - object TournamentFees: ListCriteria() - object Cash: SimpleCriteria(listOf(QueryCondition.IsCash)) - object Tournament: SimpleCriteria(listOf(QueryCondition.IsTournament)) - - val queries: List - get() { - return when (this) { - is AllMonthsUpToNow -> { - val realm = Realm.getDefaultInstance() - val firstSession= realm.where().sort("startDate", Sort.ASCENDING).findFirst() - val lastSession = realm.where().sort("startDate", Sort.DESCENDING).findFirst() - realm.close() - - val years: ArrayList = arrayListOf() - - val firstYear = firstSession?.year ?: return years - val firstMonth = firstSession.month ?: return years - val lastYear = lastSession?.year ?: return years - val lastMonth = lastSession.month ?: return years - - for (year in firstYear..lastYear) { - val currentYear = QueryCondition.AnyYear(year) - for (month in 0..11) { - - if (year == firstYear && month < firstMonth) { - continue - } - if (year == lastYear && month > lastMonth) { - continue - } - - val currentMonth = QueryCondition.AnyMonthOfYear(month) - val query = Query(currentYear, currentMonth) - years.add(query) - } - } - years - } - else -> { - return this.queryConditions - } - } - } - - val queryConditions: List - get() { - return when (this) { - is Bankrolls -> comparison() - is Games -> comparison() - is TournamentFeatures -> comparison() - is TournamentNames -> comparison() - is Locations -> comparison() - is TransactionTypes-> comparison() - is SimpleCriteria -> comparison() - is Limits -> comparison() - is TournamentTypes -> comparison() - is TableSizes -> comparison() - is TournamentFees -> comparison() - is Years -> { - val years = arrayListOf() - val realm = Realm.getDefaultInstance() - val lastSession = realm.where().sort("startDate", Sort.DESCENDING).findFirst() - val yearNow = lastSession?.year ?: return years - - realm.where().sort("year", Sort.ASCENDING).findFirst()?.year?.let { - for (index in 0..(yearNow - it)) { - val yearCondition = QueryCondition.AnyYear().apply { - listOfValues = arrayListOf(it + index) - } - years.add(Query(yearCondition)) - } - } - realm.close() - years - } - is Blinds -> comparison() - else -> throw PokerAnalyticsException.QueryTypeUnhandled - } - } - - companion object { - inline fun , reified T : NameManageable> compare(): List { - val objects = mutableListOf() - val realm = Realm.getDefaultInstance() - realm.where().findAll().forEach { - val condition = (QueryCondition.getInstance() as S).apply { - setObject(it) - } - objects.add(condition) - } - objects.sorted() - realm.close() - return objects.map { Query(it) } - } - - inline fun < reified S : QueryCondition.ListOfValues, T:Any > compareList(values:List): List { - val objects = mutableListOf() - values.forEach { - val condition =(S::class.java.newInstance()).apply { - listOfValues = arrayListOf(it) - } - objects.add(condition) - } - objects.sorted() - return objects.map { Query(it) } - } - } +sealed class Criteria : RowRepresentable { + abstract class RealmCriteria : Criteria() { + inline fun comparison(): List { + return compare, T>() + } + } + + abstract class SimpleCriteria(private val conditions: List) : Criteria() { + fun comparison(): List { + return conditions.map { Query(it) } + } + } + + abstract class ListCriteria : Criteria() { + inline fun , reified S : Comparable> comparison(): List { + QueryCondition.distinct()?.let { + val values = it.mapNotNull { session -> + when (this) { + is Limits -> if (session.limit is S) { + session.limit as S + } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue + is TournamentTypes -> if (session.tournamentType is S) { + session.tournamentType as S + } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue + is TableSizes -> if (session.tableSize is S) { + session.tableSize as S + } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue + is TournamentFees -> if (session.tournamentEntryFee is S) { + session.tournamentEntryFee as S + } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue + is Blinds -> if (session.blinds is S) { + session.blinds as S + } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue + else -> null + } + }.distinct() + return compareList(values = values) + } + return listOf() + } + } + + + object Bankrolls : RealmCriteria() + object Games : RealmCriteria() + object TournamentNames : RealmCriteria() + object Locations : RealmCriteria() + object TournamentFeatures : RealmCriteria() + object TransactionTypes : RealmCriteria() + object Limits : ListCriteria() + object TableSizes : ListCriteria() + object TournamentTypes : ListCriteria() + object MonthsOfYear : SimpleCriteria(List(12) { index -> + QueryCondition.AnyMonthOfYear().apply { listOfValues = arrayListOf(index) } + }) + + object DaysOfWeek : SimpleCriteria(List(7) { index -> + QueryCondition.AnyDayOfWeek().apply { listOfValues = arrayListOf(index + 1) } + }) + + object SessionTypes : SimpleCriteria(listOf(QueryCondition.IsCash, QueryCondition.IsTournament)) + object BankrollTypes : SimpleCriteria(listOf(QueryCondition.IsLive, QueryCondition.IsOnline)) + object DayPeriods : SimpleCriteria(listOf(QueryCondition.IsWeekDay, QueryCondition.IsWeekEnd)) + object Years : ListCriteria() + object AllMonthsUpToNow : ListCriteria() + object Blinds : ListCriteria() + object TournamentFees : ListCriteria() + object Cash : SimpleCriteria(listOf(QueryCondition.IsCash)) + object Tournament : SimpleCriteria(listOf(QueryCondition.IsTournament)) + + val queries: List + get() { + return when (this) { + is AllMonthsUpToNow -> { + val realm = Realm.getDefaultInstance() + val firstSession = realm.where().sort("startDate", Sort.ASCENDING).findFirst() + val lastSession = realm.where().sort("startDate", Sort.DESCENDING).findFirst() + realm.close() + + val years: ArrayList = arrayListOf() + + val firstYear = firstSession?.year ?: return years + val firstMonth = firstSession.month ?: return years + val lastYear = lastSession?.year ?: return years + val lastMonth = lastSession.month ?: return years + + for (year in firstYear..lastYear) { + val currentYear = QueryCondition.AnyYear(year) + for (month in 0..11) { + + if (year == firstYear && month < firstMonth) { + continue + } + if (year == lastYear && month > lastMonth) { + continue + } + + val currentMonth = QueryCondition.AnyMonthOfYear(month) + val query = Query(currentYear, currentMonth) + years.add(query) + } + } + years + } + else -> { + return this.queryConditions + } + } + } + + val queryConditions: List + get() { + return when (this) { + is Bankrolls -> comparison() + is Games -> comparison() + is TournamentFeatures -> comparison() + is TournamentNames -> comparison() + is Locations -> comparison() + is TransactionTypes -> comparison() + is SimpleCriteria -> comparison() + is Limits -> comparison() + is TournamentTypes -> comparison() + is TableSizes -> comparison() + is TournamentFees -> comparison() + is Years -> { + val years = arrayListOf() + val realm = Realm.getDefaultInstance() + val lastSession = realm.where().sort("startDate", Sort.DESCENDING).findFirst() + val yearNow = lastSession?.year ?: return years + + realm.where().sort("year", Sort.ASCENDING).findFirst()?.year?.let { + for (index in 0..(yearNow - it)) { + val yearCondition = QueryCondition.AnyYear().apply { + listOfValues = arrayListOf(it + index) + } + years.add(Query(yearCondition)) + } + } + realm.close() + years + } + is Blinds -> comparison() + else -> throw PokerAnalyticsException.QueryTypeUnhandled + } + } + + companion object { + inline fun , reified T : NameManageable> compare(): List { + val objects = mutableListOf() + val realm = Realm.getDefaultInstance() + realm.where().findAll().forEach { + val condition = (QueryCondition.getInstance() as S).apply { + setObject(it) + } + objects.add(condition) + } + objects.sorted() + realm.close() + return objects.map { Query(it) } + } + + inline fun , T : Any> compareList(values: List): List { + val objects = mutableListOf() + values.forEach { + val condition = (S::class.java.newInstance()).apply { + listOfValues = arrayListOf(it) + } + objects.add(condition) + } + objects.sorted() + return objects.map { Query(it) } + } + + val all: List + get() { + return listOf( + Bankrolls, Games, TournamentNames, Locations, + TournamentFeatures, Limits, TableSizes, TournamentTypes, + MonthsOfYear, DaysOfWeek, SessionTypes, + BankrollTypes, DayPeriods, Years, + AllMonthsUpToNow, Blinds, TournamentFees + ) + } + + + } + } diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/Filter.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/Filter.kt index 33c6de0a..a4588e04 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/Filter.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/Filter.kt @@ -3,20 +3,11 @@ package net.pokeranalytics.android.model.realm import io.realm.* import io.realm.annotations.PrimaryKey import io.realm.kotlin.where -import net.pokeranalytics.android.R import net.pokeranalytics.android.model.filter.Filterable import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.QueryCondition -import net.pokeranalytics.android.model.interfaces.CountableUsage -import net.pokeranalytics.android.model.interfaces.NameManageable -import net.pokeranalytics.android.model.interfaces.SaveValidityStatus -import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.view.RowRepresentable -import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor import net.pokeranalytics.android.ui.view.rowrepresentable.FilterCategoryRow -import net.pokeranalytics.android.ui.view.rowrepresentable.GameRow -import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow -import net.pokeranalytics.android.util.NULL_TEXT import timber.log.Timber import java.util.* @@ -51,6 +42,11 @@ open class Filter : RealmObject(), RowRepresentable { Timber.d(">>> Filter query: ${realmQuery.description}") return realmQuery.findAll() } + + fun sortedByUsage(realm: Realm): RealmResults { + return realm.where(Filter::class.java).findAll().sort("usageCount") + } + } @PrimaryKey diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/ReportSetup.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/ReportSetup.kt index 9014a1b9..246adcf1 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/ReportSetup.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/ReportSetup.kt @@ -3,12 +3,13 @@ package net.pokeranalytics.android.model.realm import io.realm.RealmList import io.realm.RealmObject import io.realm.annotations.PrimaryKey +import net.pokeranalytics.android.ui.view.RowRepresentable import java.util.* -enum class ReportDisplay { - TABLE, - GRAPH, - MAP +enum class ReportDisplay : RowRepresentable { + FIGURES, + EVO_GRAPH, + COMPARISON_GRAPH } open class ReportSetup : RealmObject() { @@ -20,7 +21,7 @@ open class ReportSetup : RealmObject() { var name: String = "" // The type of display of the report - var display: Int = ReportDisplay.TABLE.ordinal + var display: Int = ReportDisplay.FIGURES.ordinal // @todo define the configuration options diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/ReportCreationActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/ReportCreationActivity.kt new file mode 100644 index 00000000..cde22ea1 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/ReportCreationActivity.kt @@ -0,0 +1,24 @@ +package net.pokeranalytics.android.ui.activity + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import net.pokeranalytics.android.R +import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity + +class ReportCreationActivity : PokerAnalyticsActivity() { + + companion object { + fun newInstance(context: Context) { + val intent = Intent(context, ReportCreationActivity::class.java) + context.startActivity(intent) + } + + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_reportcreation) + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportCreationFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportCreationFragment.kt new file mode 100644 index 00000000..fc25d23a --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportCreationFragment.kt @@ -0,0 +1,102 @@ +package net.pokeranalytics.android.ui.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import io.realm.Realm +import net.pokeranalytics.android.R +import net.pokeranalytics.android.calculus.Stat +import net.pokeranalytics.android.model.Criteria +import net.pokeranalytics.android.model.realm.Filter +import net.pokeranalytics.android.model.realm.ReportDisplay +import net.pokeranalytics.android.ui.fragment.components.RealmFragment +import net.pokeranalytics.android.ui.view.RowRepresentable + +class ReportCreationFragment : RealmFragment() { + + class Process { + + var step: Step = Step.TYPE + var display: ReportDisplay? = null + var stats = listOf() + var comparators = listOf() + var useFilter: Boolean? = null + var filter: Filter? = null + + enum class Step { + TYPE, + STAT, + COMPARATOR, + FILTER, + FINALIZE + } + + val nextStep: Step + get() { + return when (this.display) { + null -> Step.TYPE + else -> { + if (this.stats.isEmpty()) { + Step.STAT + } else if (this.display!! == ReportDisplay.COMPARISON_GRAPH && this.comparators.isEmpty()) { + Step.COMPARATOR + } else if (this.useFilter == null) { + Step.FILTER + } else { + Step.FINALIZE + } + } + } + } + + fun titleForStep(step: Step) : Int? { + return when (step) { + Step.TYPE -> R.string.new_report_step_type + Step.STAT -> R.string.new_report_step_stat + Step.COMPARATOR -> R.string.new_report_step_comparator + Step.FILTER -> R.string.new_report_step_filter + else -> null + } + } + + val dataSource: List + get() { + return when (this.step) { + Step.TYPE -> return listOf(ReportDisplay.FIGURES, ReportDisplay.EVO_GRAPH, ReportDisplay.COMPARISON_GRAPH) + Step.STAT -> return Stat.values().toList() + Step.COMPARATOR -> Criteria.all + Step.FILTER -> { + val realm = Realm.getDefaultInstance() + val filters = Filter.sortedByUsage(realm) + realm.close() + filters + } + else -> listOf() + } + } + + val nextButtonShouldAppear: Boolean + get() { + return when (this.step) { + Step.STAT, Step.COMPARATOR, Step.FILTER -> true + else -> false + } + } + + val nextButtonTitle: Int + get() { + return when (this.step) { + Step.FILTER -> R.string.launch_report + else -> R.string.next + } + } + + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) + return inflater.inflate(R.layout.fragment_report_creation, container, false) + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportsFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportsFragment.kt index 73301aba..17a39180 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportsFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportsFragment.kt @@ -7,7 +7,8 @@ import android.view.ViewGroup import android.widget.Toast import androidx.recyclerview.widget.LinearLayoutManager import io.realm.Realm -import kotlinx.android.synthetic.main.fragment_stats.* +import kotlinx.android.synthetic.main.fragment_data_list.* +import kotlinx.android.synthetic.main.fragment_stats.recyclerView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -16,6 +17,7 @@ import net.pokeranalytics.android.calculus.Calculator import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.model.Criteria import net.pokeranalytics.android.model.combined +import net.pokeranalytics.android.ui.activity.ReportCreationActivity import net.pokeranalytics.android.ui.activity.ReportDetailsActivity import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate @@ -100,6 +102,11 @@ class ReportsFragment : PokerAnalyticsFragment(), StaticRowRepresentableDataSour layoutManager = viewManager adapter = reportsAdapter } + + this.addButton.setOnClickListener { + ReportCreationActivity.newInstance(requireContext()) + } + } /** diff --git a/app/src/main/res/layout/activity_reportcreation.xml b/app/src/main/res/layout/activity_reportcreation.xml new file mode 100644 index 00000000..c99960dd --- /dev/null +++ b/app/src/main/res/layout/activity_reportcreation.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_report_creation.xml b/app/src/main/res/layout/fragment_report_creation.xml new file mode 100644 index 00000000..bcd331ff --- /dev/null +++ b/app/src/main/res/layout/fragment_report_creation.xml @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_reports.xml b/app/src/main/res/layout/fragment_reports.xml index aaa1d704..461dce4c 100644 --- a/app/src/main/res/layout/fragment_reports.xml +++ b/app/src/main/res/layout/fragment_reports.xml @@ -15,4 +15,17 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 566249eb..67ca7946 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,6 +24,11 @@ Support We try to answer as quickly as we can, in english or french ! Loading, please wait… + Select your type of report + Select one or more statistics + Select one or more comparison criteria + Select a filter if you want, or launch report + Launch Report Address Naming suggestions