# ---------------------------
# Exercise 1: Set Operations
# ---------------------------
print("Exercise 1:")

# Create sets and rename them for easier reference
students_icc = {"Jeremy", "Victoria", "Paul"}
students_in = {"Victoria", "Alexander"}
students_all = {"Jeremy", "Victoria", "Paul", "Alexander"}

# Union of two sets
print("Union between students_icc and students_in:")
print(students_icc | students_in)  # Using operator
#print(students_icc.union(students_in))  # Using method

# Intersection of two sets
print("Intersection between students_icc and students_in:")
print(students_icc & students_in)  # Using operator
#print(students_icc.intersection(students_in))  # Using method

# Symmetric difference (elements in either set but not both)
print("Symmetric Difference between students_icc and students_in:")
print(students_icc ^ students_in)  # Using operator
#print(students_icc.symmetric_difference(students_in))  # Using method

# Difference (elements in one set but not the other)
print("Difference:")
print("students_icc - students_in:")
print(students_icc - students_in)  # Using operator
#print(students_icc.difference(students_in))  # Using method

print("students_in - students_icc:")
print(students_in - students_icc)
#print(students_in.difference(students_icc))

# Difference between students_icc and students_all
print("students_icc - students_all:")
print(students_icc - students_all)  # Empty set
#print(students_all - students_icc)

# ---------------------------
# Exercise 2: Types and orders in sets
# ---------------------------

set_a = {1, "ABC", -7.2, True, 1, 1, 1}
set_b = {-7.2, 1, True, "ABC"}

print("\nExercise 2:")

print("Are these two sets equal ?")
print(set_a == set_b)

# Explanation :
# Sets can store elements from any type, and just like lists can store elements from different types in the same set. 
# Any element added multiple times in a set will only be stored once.
# Order also does not matter. Two sets with the same elements but in different order are equal !


# ---------------------------
# Exercise 3: Eliminating Duplicates
# ---------------------------
print("\nExercise 3:")

# Demonstrating set behavior with duplicate elements
lst = ["foo.py", "bar.py", "a", "b", "foo.py", "b"]
unique_elements = set(lst)
print("Original list:", lst)
print("List (duplicates removed):", list(unique_elements))

# ---------------------------
# Exercise 4: Frozensets and Immutability
# ---------------------------
print("\nExercise 4:")

# Creating a frozenset to prevent modifications
frozen_students = frozenset(students_icc)

# Demonstrating use case for frozenset
students_icc.add("test")  # Works for regular set
print("Updated students_icc:", students_icc)

# Frozensets are immutable and do not allow adding or removing elements
# Uncommenting the following line will raise an AttributeError:
# frozen_students.add("test")

# Explanation for the example of using frozenset:
# - Prevent new students from being added after the course starts.
# - Sets ensure no duplicate student entries.
# - Set operations help find students in multiple courses or check graduation requirements.
# Overall, we use frozenset when we want to ensure no changes to the set are possible.
