diff --git a/Project.toml b/Project.toml index 697a8f1..90c9ad7 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "CurricularAnalytics" uuid = "593ffa3d-269e-5d81-88bc-c3b6809c35a6" authors = ["Greg Heileman ", "Hayden Free "] -version = "2.0.0" +version = "2.1.0" [deps] CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" diff --git a/src/CurricularAnalytics.jl b/src/CurricularAnalytics.jl index 2bbee93..64d6925 100644 --- a/src/CurricularAnalytics.jl +++ b/src/CurricularAnalytics.jl @@ -26,14 +26,14 @@ include("DegreePlanCreation.jl") include("Simulation/Simulation.jl") include("RequirementsAnalytics.jl") -export AA, AAS, AS, AbstractCourse, AbstractRequirement, BA, BS, Course, CourseCollection, CourseCatalog, CourseRecord, CourseSet, credit_balance, Curriculum, DegreePlan, - EdgeClass, Enrollment, Grade, LearningOutcome, PassRate, RequirementSet, Requisite, Student, StudentRecord, Simulation, System, Term, TransferArticulation, +export AA, AAS, AS, AbstractCourse, AbstractRequirement, add_no_multi_use!, BA, BS, Course, CourseCollection, CourseCatalog, CourseRecord, CourseSet, credit_balance, Curriculum, + DegreePlan, EdgeClass, Enrollment, Grade, LearningOutcome, PassRate, RequirementSet, Requisite, Student, StudentRecord, Simulation, System, Term, TransferArticulation, add_course!, add_lo_requisite!, add_requisite!, add_requisite_clause!, add_transfer_catalog, add_transfer_course, all_paths, back_edge, basic_metrics, basic_statistics, - bin_filling, blocking_factor, centrality, co, compare_curricula, convert_ids, complexity, course, course_from_id, course_from_vertex, course_id, - courses_from_vertices, create_degree_plan, cross_edge, dead_ends, delay_factor, delete_requisite!, dfs, extraneous_requisites, find_term, forward_edge, - gad, grade, homology, is_duplicate, is_valid, isvalid_curriculum, isvalid_degree_plan, level, longest_path, longest_paths, merge_curricula, pass_table, passrate_table, - pre, postorder_traversal, preorder_traversal, print_plan, quarter, reach, reach_subgraph, reachable_from, reachable_from_subgraph, reachable_to, reachable_to_subgraph, - read_csv, requisite_distance, requisite_type, semester, set_passrates, set_passrate_for_course, set_passrates_from_csv, show_requirements, similarity, simple_students, + bin_filling, blocking_factor, centrality, co, compare_curricula, convert_ids, complexity, course, course_from_id, course_from_vertex, course_id, courses_from_vertices, + create_degree_plan, cross_edge, dead_ends, delay_factor, delete_requisite!, dfs, extraneous_requisites, find_term, forward_edge, gad, grade, homology, is_duplicate, + is_valid, isvalid_curriculum, isvalid_degree_plan, level, longest_path, longest_paths, merge_curricula, pass_table, passrate_table, pre, postorder_traversal, + preorder_traversal, print_plan, quarter, reach, reach_subgraph, reachable_from, reachable_from_subgraph, reachable_to, reachable_to_subgraph, read_csv, remove_no_multi_use!, + requisite_distance, requisite_type, semester, set_passrates, set_passrate_for_course, set_passrates_from_csv, show_requirements, similarity, simple_students, simulate, simulation_report, strict_co, topological_sort, total_credits, transfer_equiv, tree_edge, write_csv, knowledge_transfer, csv_stream, remove_course! """ diff --git a/src/DataTypes/Requirements.jl b/src/DataTypes/Requirements.jl index f98e690..c385f19 100644 --- a/src/DataTypes/Requirements.jl +++ b/src/DataTypes/Requirements.jl @@ -129,8 +129,7 @@ Keyword: - `num_regex::Regex` : regular expression for matching a course number in the course catalog. Default is `".*"`, i.e., match any character any number of times. - `min_grade::Grade` : The minimum letter grade that must be earned in courses satisfying the regular expressions. -- `double_count::Bool` : Specifies whether or not each course in the course set can be used to satisfy other requirements - that contain any of the courses in this `CourseSet`. Default = false +- `no_multi_use:Set` : The course sets in this set are mutually exclusive with this course set # Examples: ```julia-repl @@ -149,13 +148,14 @@ mutable struct CourseSet <: AbstractRequirement prefix_regex::Regex # Regular expression for matching a course prefix in the course catalog. num_regex::Regex # Regular expression for matching a course number in the course catalog, must satisfy both min_grade::Grade # The minimum letter grade that must be earned in courses satisfying the regular expressions - double_count::Bool # Each course in the course set can satisfy any other requirement that has the same course. Default = false + no_multi_use::Set{CourseSet} # Each course in this course set object can either be assigned to this course set or to a course set in the no_multi_use set, but not both # Constructor # A requirement may involve a set of courses, or a set of requirements, but not both + #TODO: Check that a CourseSet cannot include itself in its no_multi_use set, possibly using setproperty! function CourseSet(name::AbstractString, credit_hours::Real, course_reqs::Array{Pair{Course,Grade},1}=Array{Pair{Course,Grade},1}(); description::AbstractString="", course_catalog::CourseCatalog=CourseCatalog("", ""), prefix_regex::Regex=r".^", num_regex::Regex=r".^", course_regex::Regex=r".^", - min_grade::Grade=grade("D"), double_count::Bool=false) + min_grade::Grade=grade("D"), double_count::Union{Bool,Nothing}=nothing, no_multi_use::Set{CourseSet}=Set{CourseSet}()) # r".^" is a regex that matches nothing this = new() this.name = name @@ -166,7 +166,10 @@ mutable struct CourseSet <: AbstractRequirement this.course_catalog = course_catalog this.prefix_regex = prefix_regex this.num_regex = num_regex - this.double_count = double_count + if(!isnothing(double_count)) + printstyled("WARNING: Use of double_count in course set $(this.name) has been depreciated in lieu of no_multi_use.\n", color = :yellow) + end + this.no_multi_use = no_multi_use for c in course_catalog.catalog # search the supplied course catalog for courses satisfying both prefix and num regular expressions if occursin(prefix_regex, c[2].prefix) && occursin(num_regex, c[2].num) push!(course_reqs, c[2] => min_grade) @@ -189,6 +192,32 @@ mutable struct CourseSet <: AbstractRequirement end end +""" +Add CourseSets to the `no_multi_use` set in a target Courseset. A set union is performed between the set passed in +as the second argument, and the no_multi_use set in the target Courseset, + + add_no_multi_use!(target::CourseSet, course_sets::Set{CourseSet}) +""" +function add_no_multi_use!(target::CourseSet, course_sets::Set{CourseSet}) + for cs ∈ course_sets + if cs == target # make sure the target CourseSet is NOT in the set of additioal CourseSets + delete!(course_sets, cs) # if it is, then remove it from additional_course_sets + printstyled("WARNING: Attempting to add $(cs.name) to its own no_multi_use set. This is a logical error, $(cs.name) will not be added to its no_mulit_use set.\n", color = :yellow) + end + end + target.no_multi_use = union(target.no_multi_use, course_sets) +end + +""" +Remove CourseSets from the `no_multi_use` set in a target Courseset. A set difference is performed between no_multi_use set +in the target Courseset, and the set passed in as the second argument, + + remove_no_multi_use!(target::CourseSet, course_sets::Set{CourseSet}) +""" +function remove_no_multi_use!(target::CourseSet, course_sets::Set{CourseSet}) + target.no_multi_use = setdiff(target.no_multi_use, course_sets) # returns those course sets present in target.no_multi_use but not in course_sets +end + """ The `RequirementSet` data type is used to represent a collection of requirements. To instantiate a `RequirementSet` use: diff --git a/test/DataTypes.jl b/test/DataTypes.jl index f8b2d8c..fd7496a 100644 --- a/test/DataTypes.jl +++ b/test/DataTypes.jl @@ -157,11 +157,9 @@ add_course!(CCat, [E,F,G]); cs1 = CourseSet("Test Course Set 1", 3, [(A=>grade("C")), (B=>grade("D"))], course_catalog=CCat, prefix_regex=r"^\s*+EGR\s*+$", num_regex=r".*", double_count=true); @test cs1.name == "Test Course Set 1" @test cs1.course_catalog == CCat -@test cs1.double_count == true @test length(cs1.course_reqs) == 3 # The regex's specified will match all courses with number 111 and any prefix cs2 = CourseSet("Test Course Set 2", 3, Array{Pair{Course,Grade},1}(), course_catalog=CCat, prefix_regex=r".*", num_regex=r"^\s*+111\s*+$"); -@test cs2.double_count == false @test length(cs2.course_reqs) == 1 req_set = AbstractRequirement[cs1,cs2]; @@ -208,4 +206,57 @@ add_transfer_course(ta, [A.id], XCat2.id, XB.id) sim_obj = Simulation(dp); @test sim_obj.degree_plan == dp +# ------------------------------------------------------------ +# no_multi_use cannot contain itself +# Intent: a CourseSet must never be mutually-exclusive with itself +# (self-exclusion is meaningless and can create confusing constraints later). +# ------------------------------------------------------------ + @testset "no_multi_use cannot contain itself" begin + C1 = Course("CourseA", 3) + + # Attempt to incorrectly include cs1 in its own no_multi_use set + cs1 = CourseSet( + "cs1", + 3, + [C1 => grade("D")], + description="", + no_multi_use=Set{CourseSet}() # start empty; we'll add a course set later + ) + + # User mistake: trying to add course set to its own no_multi_use set + add_no_multi_use!(cs1, Set([cs1])) + + # "Proof" expectation: implementation should prevent this (by auto-removing). + @test !(cs1 ∈ cs1.no_multi_use) # must be false if the rule is enforced + end + +# ------------------------------------------------------------ +# add_no_multi_use! testing +# add course sets to the no_multi_use set. +# ------------------------------------------------------------ + @testset "add_no_multi_use! and remove_no_multi_use!" begin + C1 = Course("CourseA", 3) + + # create two course set + cs1 = CourseSet( + "cs1", + 3, + [C1 => grade("D")], + ) + + cs2 = CourseSet( + "cs2", + 3, + [C1 => grade("D")], + ) + + # add a course set + add_no_multi_use!(cs1, Set([cs2])) + @test cs2 ∈ cs1.no_multi_use + + # now remove it + remove_no_multi_use!(cs1, Set([cs2])) + @test !(cs2 ∈ cs1.no_multi_use) + end + end