Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "CurricularAnalytics"
uuid = "593ffa3d-269e-5d81-88bc-c3b6809c35a6"
authors = ["Greg Heileman <gregheileman@gmail.com>", "Hayden Free <haydenwfree@gmail.com>"]
version = "2.0.0"
version = "2.1.0"

[deps]
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
Expand Down
14 changes: 7 additions & 7 deletions src/CurricularAnalytics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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!

"""
Expand Down
39 changes: 34 additions & 5 deletions src/DataTypes/Requirements.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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:

Expand Down
55 changes: 53 additions & 2 deletions test/DataTypes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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
Loading