This document outlines the language features of the “Hanson Format”, a domain-specific language (DSL) intended for use in statically defining the requirements for areas of study at St. Olaf College.
An “area of study” is any single degree, major, concentration, or area of emphasis.
Credit for the original impetus for investigating a static format, instead of writing imperative routines for each requirement, is due to Professor Bob Hanson, of the Chemistry Department, who led an independent study into this topic in January of 2015.
how to use multiple values to search (MCD | MCG) |
term
A specific year/semester combination; i.e., 2012-1
.
area of study
A degree, major, concentration, or area of emphasis.
Each area of study is broken down into one or more sections or Requirements.
The top-level of the file must follow the following structure:
name: Chemistry
type: degree | major | concentration | emphasis
catalog: 2015-16
result: Rule
requirements:
< Sections and requirements go here. >
Each section consists of either sub-sections or Requirements.
Each Requirement must contain at least the result
key, which is a Rule (rules are described in the next section.)
A Section may contain only the message
, result
, and requirements
keys.
If a section is nested within another section, it is known as a “sub-section”.
A Section follows the following structure:
Name:
result: Rule
requirements:
<other sections / requirements>
A Requirement follows the following structure:
Name:
message: optional; a description of the requirement
contract: optional; true|false; if true, the requirement is a contract between the student and the department
department_audited: optional; true|false; if true, the requirement is tracked and audited by the department, not the registrar or SIS
result: a Rule
save: optional; an Array of Saved Subsets
Rules are the basic building blocks of areas of study.
A course
rule states that a course must exist in the student’s plan. If the student’s plan includes the course listed, the rule will be fulfilled; otherwise, it will not.
Most requirements should consist primarily of
course:
rules.
course: CSCI 121
Most Rules support referring to course
rules by way of a shorthand where you leave out the course:
prefix.
I.E., the following two rules are identical:
count: any
of:
- {course: CSCI 121}
count: any
of:
- CSCI 121
course
propertiesThe verbose form of the course
rule also supports additional keys, for certain scenarios:
term:
year-semester; specifies an exact term in which the course must have been taken; useful for topics coursessection:
letter; specifies the exact section for the courseyear:
numbersemester:
numberlab: true |
false |
international: true |
false |
A more verbose form of a course, then, looks like this:
- {course: CSCI 121, section: A, lab: true, term: 2012}
In order to refer to the result of a requirement, you must combine the key requirement:
with the name of the requirement.
If you have
requirements:
Requirement Name:
result: <blah>
then you can refer to the result of that requirement as follows:
requirement: Requirement Name
You can also mark a requirement as “optional”, which makes it completely optional.
To do so, add optional: true
to the requirement:
- {requirement: Requirement Name, optional: true}
count: Number | any | all
of: Array<Rule>
The of
key takes an array of other Rules; count
determines how many of the rules must evaluate to true
in order for this rule to also evaluate to true
.
The of/count
rule is greedy; it does not end the evaluation process when the count is reached. Instead, it evaluates every possibility in the of
array.
That is to say, count: 1, of: [<four courses>]
will consume all four of those courses.
There is not currently a way to change this behavior. It remains to be seen if it is problematic.
count: all
of:
- requirement: Requirement Name
- course: CSCI 123
- ASIAN 140
There are three valid values for the count:
key: any positive, non-zero integer; the keyword “any”; or the keyword “all”.
both: [Rule, Rule]
either: [Rule, Rule]
both:
and either:
are specializations of the count:,of:
rule; you could write both:
as count:all,of:Rules
, and either:
as count:any,of:Rules
. Sometimes it’s just easier to be able to say “yeah, I want both of these things to be true”.
both:
and either:
both require exactly two Rules as their input.
TODO(hawken): do I need/want an all:
/ any:
shorthand pair too? Maybe… it’s attractive.
either: [CSCI 121, CSCI 125]
both: [CSCI 251, {course: CSCI 252, lab: true}]
given:
is the workhorse rule; when you need something that’s not feasible to describe statically, such as all of the FYW courses, given:
is able to search through the student’s planned courses at audit time.
There are several variants of the given:
rule, each of which are described below.
In general, a given
rule takes a set of Inputs (given:
), filters them through the given Filters (where:
), selects the chosen type of Output for each item (what:
), then executes the specific Action on said items (do:
).
# “There must be 6 or more courses with the `FYW` gereq attribute in the student's plan”
given: courses
where: {gereqs: FYW}
what: courses
do: count >= 6
Each given:
rule needs some type of input on which to execute. The given:
key tells the rule what to execute on.
given: courses
The :courses
value will provide the set of all courses from the student’s plan.
given: these courses
courses: [CSCI 121, ASIAN 130]
repeats: first | last | all
The :these courses
value requires that the courses:
and repeats:
keys be provided.
It provides the intersection between the student’s planned courses and the set of courses listed under courses:
.
By specifying the repeats:
key, you can control whether it will emit the first occurrence, the last occurrence, or all occurrences.
given: these requirements
requirements: Array<RequirementName>
The :these requirements
value requires that the requirements:
key be provided.
It provides the set of courses which were used by the named requirements.
given: areas of study
The :areas of study
value provides the student’s areas of study – their degrees, majors, concentrations, and areas of emphasis.
given: save
save: "a named save block"
The :save
value requires that the save:
key be defined at the requirement level.
The save: "a named save block"
value provides contents of that save block, as defined in the save:
section of the current rule.
See the “Saving Subsets” section for more information.
A given:
rule may need to filter the input data before it runs the action. The where:
key tells the rule what data should be included or excluded.
The general syntax is as follows:
where: {key: value, key2: value2}
where key
may be one of “department”, “semester”, “year”, “level”, “institution”, “number”, “graded”, or “gereqs”.
value
may be a simple value, like MATH
or FYW
or 2018
or true
; it may be a keyword, like graduation-year
; or it may be an operation, like >= 2018
or ! MATH
.
given: courses
where: {department: MATH}
given: courses
where: {department: "! MATH"}
where:{department}
filters the input down to only courses which have the given department.
TODO(hawken): what happens to AS/PS? is the department AS/PS, ASIAN, PSCI, or [ASIAN, PSCI]?
given: courses
where: {semester: Interim}
where:{semester}
filters the input down to only courses which were taken in the given semester.
Valid values are as follows: Fall, Interim, Spring, Summer Session 1, Summer Session 2.
given: courses
where: {year: graduation-year}
where:{year}
filters the input down to only courses which were taken in the given year.
Valid values are as follows: graduation-year
.
given: courses
where: {level: '>= 200'}
where:{level}
filters the input down to only courses which were at the given level.
given: courses
where: {institution: St. Olaf College}
where:{institution}
filters the input down to only courses which were taken at the given institution.
given: courses
where: {graded: true}
where:{graded}
filters the input down to only courses which were taken graded (in other words, courses which were not p/n or s/u).
TODO(hawken) er… yeah? what is a graded course? double-check.
given: courses
where: {gereqs: FYW}
where:{gereqs}
filters the input down to only courses which have the given GE Requirement as an attribute.
In other words, given the filter where: {gereqs: FYW}
, any course which has FYW in its gereqs attribute (i.e., {gereqs: [FYW, WRI, HBS]
) will pass the filter.
These work similarly to the where:{gereqs}
filter, but with some added wrinkles described in the “Custom Attributes on Courses” section.
given: courses
where: {math_perspective: 'A'}
Sometimes, you need to specify more than just a single value; to that end, you can specify either a less-than/greater-than operation or a boolean “or” as the value of a where{thing}
.
given: courses
where: {gereqs: 'MCD | MCG'}
given: courses
where: {level: '>= 200'}
If you need to restrict a certain type of course from being counted, such as Asian Studies’ requirement that “no more than two level-I courses may count”, you can attach a “limiter” to the rule.
given: courses
limit:
- where: {level: '100'}
at_most: 2
do: count >= 6
For more information, see the “Limiters” section.
TODO(hawken): Talk about how a limiter differs from a
where:
clause.Types of Output
Each
given:
rule must output some information. Thewhat:
key tells the rule what to output.
given: courses
what: courses
This returns each course.
given: courses
what: credits
This returns the number of credits for each course.
given: courses
what: distinct courses
TODO(hawken): do we need “distinct” courses? I think I need to look at what makes it “distinct” again, because I have a feeling that the default type of a course might need to change, and we may need a “duplicated courses” value instead…
given: courses
what: grades
This returns the grade that the student got from each course.
given: courses
what: terms
This returns the term in which the course was taken for each course.
given: areas of study
what: areas of study
This returns each area of study.
Each given:
rule needs to execute some type of action. The do:
key tells the rule what to do.
Given an input sequence of items, count
becomes the number of items in the sequence.
given: courses
what: courses
do: count >= 6
Given an input sequence of numbers, sum
becomes the sum total of the items in the sequence.
given: courses
what: credits
do: sum >= 6
It’s important to highlight the difference between what:credits, do:count
and what:credits, do:sum
; do:count
will return the number of courses with credits, while do:sum
will tally them up and return the number of credits.
todo(hawken): can we somehow merge
do:count
anddo:sum
? it’s never been confusing in hanson@v1…
Given an input sequence of numbers, average
becomes the average of the items in the sequence.
given: courses
what: grades
do: average >= 2.0
Given an input sequence of numbers, minimum
becomes the smallest item in the sequence.
given: courses
what: terms
do: minimum
This is mostly useful to save values for a later $variable_name
action.
Given a two-item input sequence of numbers, difference
becomes the difference between the two items.
given: these courses
courses: [DANCE 201, DANCE 212]
what: term
do: difference <= 1
Writing $variable_name
substitutes the calculated value of that variable into the equation.
do: $first_btst < $first_ein
The less-than operator, <
, returns “true” if the left side is less than the right side; otherwise, it returns “false”.
It requires that both sides be numeric.
The greater-than operator, >
, returns “true” if the left side is greater than the right side; otherwise, it returns “false”.
It requires that both sides be numeric.
The equal-to operator, =
, returns “true” if the left side is equal to the right side; otherwise, it returns “false”.
It requires that both sides be numeric.
You can “save” partial results for re-use later in the computation.
Additionally, saving subsets can make the student’s experience nicer, because it can display intermediate results of the computations.
A save:
block may contain one or more -save
rules.
A -save
rule is similar to a given:
rule, with a few differences:
name:
keydo:
key is optionalsave:
- given: courses
where: {department: MATH}
what: courses
name: "The MATH Courses"
You can use any -save
rule in any given:
block at the same level, or it can serve as input to a subsequent -save
rule.
Save the AMCON or MATH courses, and assert that there are at least six of them.
save:
- given: courses
where: {department: 'MATH | AMCON'}
what: courses
name: "either AMCON or MATH"
result:
given: save
save: "either AMCON or MATH"
what: courses
do: count >= 6
Assert that the student took their first WRI before their first BTS-T.
save:
- given: courses
where: {gereqs: WRI}
what: terms
do: minimum
name: "the first WRI"
- given: courses
where: {gereqs: BTS-T}
what: terms
do: minimum
name: "the first BTS-T"
result:
do: >
"the first WRI" < "the first BTS-T"
Sometimes, a major needs to say “You may only count at most 2 non-MATH courses towards this major”.
To do this, you can impose a “global limit”, where the algorithm will stop using courses that match the given filters after the limit has been hit.
For instance, this block prevents more than two non-MATH courses from being selected to fulfill any requirement within the major.
limiters:
- where: {department: '! MATH'}
at_most: 2
You may also place limiters within a given
rule, as described in the “Limiting Results” section.
This rule prevents more than two level-I courses from being selected as part of the 6 courses needed to fulfill the requirement.
given: courses
limit:
- where: {level: '100'}
at_most: 2
do: count >= 6
A limiter can currently only apply an at_most
limit; other modes may be added in the future, if needed.
Note: limiter values must always be wrapped in single-quotes (that is, YAML must parse them as strings.)
Custom Attributes on Courses
Sometimes, it’s easier to specify courses by where: {attribute: value}
as opposed to listing them individually in a the requirement.
To attach an attribute for courses within a major, you declare them at the top-level of the file:
attributes:
courses:
MATH 226: [math_perspective_c]
MATH 230: [math_perspective_a, math_perspective_m]
MATH 232: [math_perspective_d]
MATH 236: [math_perspective_m]
MATH 262: [math_perspective_c, math_perspective_d, math_perspective_m]
That is, you declare a top-level dictionary called “attributes”, which contains another dictionary, named “courses”.
The left side of each line (each “key”) in the courses
dictionary is mapped to an array of attribute names. If an attribute is named in this array, then the course will have that attribute; if it’s not, then it won’t.
Custom attributes are attached to each course under the “attributes” key, so they can be matched with where: {attributes: math_perspective_a}
.
You can attach a message:
to a requirement, section, or sub-section, in order to show a message to the student.
requirements:
Core:
message: >
A thing, with some description.
Certain requirements cannot be statically codified because they are defined individually per-student.
Currently, contracts are entered into by a student and a department, and they are stored and verified by the department.
To mark a requirement as a contract, you just add contract: true
to the requirement.
requirements:
Core:
message: A contract thing.
contract: true
Certain requirements are tracked by the department, instead of the registrar.
To define a department-audited requirement, add the department_audited
key to the requirement.
requirements:
Core:
message: a message about the requirement
department_audited: true
TODO(hawken)
Assert that the student has taken a given course more than once.
result:
given: these courses
courses: [THEAT 233]
what: courses
do: count >= 1
If you need to filter or limit certain courses in an of:
rule, I recommend switching to a given:these courses
rule instead.
For example, you could do this to limit the student to only take one non-Philosophy course from the set of electives.
given: these courses
courses: [PHIL 118, PHIL 119, PHIL 120, GCON 218, REL 147]
limit:
- where: {department: '! PHIL'}
at_most: 1
do: count >= 3