dbis-functional-dependencies 0.0.7__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1761 @@
1
+ """
2
+ Created on 25.05.2022
3
+
4
+ @author: maxwellgerber
5
+
6
+ see https://gist.github.com/maxwellgerber/4caae07161ea66123de4d6c374387786
7
+
8
+ """
9
+ from itertools import combinations, permutations, chain
10
+ import numpy
11
+ import datetime
12
+ from dbis_functional_dependencies.fdsbase import Attribute, Set, Notation, FD
13
+
14
+
15
+ class FunctionalDependencySet:
16
+ """
17
+ a functional dependency set
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ attributes: str = "",
23
+ title: str = "",
24
+ description: str = "",
25
+ notation: Notation = None,
26
+ debug: bool = False,
27
+ ):
28
+ """
29
+ constructor
30
+
31
+ Args:
32
+ attributes(str): a string of attribute variable names of the scheme
33
+ title(str): a title for this functional Dependency Set
34
+ description(str): a description for this functional Dependency Set
35
+ notation(Notation): the notation to be used
36
+ debug(bool): if True switch debugging on
37
+ """
38
+ self.title = title
39
+ self.description = description
40
+ if notation is None:
41
+ notation = Notation.utf8
42
+ self.notation = notation
43
+ self.debug = debug
44
+ self.isodate = datetime.datetime.now().isoformat()
45
+ # list of FDs of the scheme. An FD is stored as a tuple (x, y), meaning x -> y
46
+ self.dependencies = []
47
+
48
+ # set of attributes of the scheme
49
+ self.attributes = Set()
50
+ self.attribute_map = {}
51
+
52
+ self.isdecomposed = False
53
+
54
+ for attr in attributes:
55
+ self.add_attribute(attr)
56
+
57
+ def __iter__(self):
58
+ """
59
+ makes me iterable
60
+ """
61
+ for fd in self.dependencies:
62
+ yield fd
63
+
64
+ def __str__(self):
65
+ """
66
+ return my text representation
67
+ """
68
+ text = self.stringify_dependencies()
69
+ return text
70
+
71
+ def set_list_as_text_list(self, set_list: list, notation: Notation):
72
+ """
73
+ convert a list of sets to a list of strings using the given delimiter
74
+
75
+ Args:
76
+ set_list(list): list of sets
77
+ notation(Notation): the notation to use
78
+
79
+ Returns:
80
+ list: of stringified sets
81
+ """
82
+ text_list = []
83
+ for a_set in set_list:
84
+ text_list.append(Set.stringify_set(a_set, notation=notation))
85
+ text_list = sorted(text_list)
86
+ return text_list
87
+
88
+ def copy(self):
89
+ fds = FunctionalDependencySet()
90
+ fds.title = self.title
91
+ fds.description = self.description
92
+ fds.notation = self.notation
93
+ fds.debug = self.debug
94
+ fds.attributes = self.attributes.copy()
95
+ fds.attribute_map = self.attribute_map.copy()
96
+ fds.dependencies = self.dependencies.copy()
97
+ fds.isdecomposed = self.isdecomposed
98
+ return fds
99
+
100
+ def stringify_dependencies(self, fdsToStringify: list = []):
101
+ """
102
+ stringifies the set of dependencies
103
+
104
+ Args:
105
+ fdsToStringify(list): set of fds in case only specific dependencies are needed
106
+ """
107
+ text = "{"
108
+ delim = ""
109
+ if self.notation == Notation.math or self.notation == Notation.plain:
110
+ fdNotation = self.notation
111
+ else:
112
+ fdNotation = Notation.utf8
113
+
114
+ if fdsToStringify == []:
115
+ fdsToStringify = self.dependencies
116
+ for left, right in fdsToStringify:
117
+ fd = FD(left, right)
118
+ fdtext = FD.stringify_FD(fd, fdNotation)
119
+ text += f"{delim}{fdtext}"
120
+ delim = ","
121
+ text += "}"
122
+ return text
123
+
124
+ def add_attribute(
125
+ self, attr_var: str, attr_english_name: str = None, attr_german_name: str = None
126
+ ):
127
+ """
128
+ add attribute to the attribute set of the scheme
129
+
130
+ Args:
131
+ attr_var(string): attribute variable name to be added to the scheme
132
+ attr_english_name(string): the name of the attribute in english
133
+ attr_german_name(string): the name of the attribute in german
134
+ """
135
+ if attr_english_name is None:
136
+ attr_english_name = attr_var
137
+ if attr_german_name is None:
138
+ attr_german_name = attr_english_name
139
+ attr = Attribute(attr_var, attr_english_name, attr_german_name)
140
+ self.add_an_attribute(attr)
141
+
142
+ def add_an_attribute(self, attr):
143
+ """
144
+ add the given Attribute
145
+ """
146
+ self.attributes.add(attr.var_name)
147
+ self.attribute_map[attr.var_name] = attr
148
+
149
+ def get_attribute(self, var_name):
150
+ return self.attribute_map[var_name]
151
+
152
+ def add_dependency(self, pre, post):
153
+ """
154
+ add dependency to the dependency list of the scheme
155
+
156
+ Args:
157
+ pre(set): attributes that initiate the FD (left of the arrow)
158
+ post(set): attributes that are determined by the FD (right of the arrow)
159
+ """
160
+ for i in chain(pre, post):
161
+ if i not in self.attributes:
162
+ # exception when an attribute is used that is not in the list of attributes of the dependency
163
+ raise Exception(f"Attribute {i} does not exist")
164
+ self.dependencies.append((set(pre), set(post)))
165
+
166
+ def remove_dependency(self, pre, post):
167
+ """
168
+ remove dependency from the dependency list of the scheme
169
+
170
+ Args:
171
+ pre(str): attributes that initiate the FD (left of the arrow)
172
+ post(str): attributes that are determined by the FD (right of the arrow)
173
+ """
174
+ for i in chain(pre, post):
175
+ if i not in self.attributes:
176
+ # exception when an attribute is used that is not in the list of attributes of the dependency
177
+ raise Exception(f"Attribute {i} does not exist")
178
+ self.dependencies.remove((set(pre), set(post)))
179
+
180
+ def get_attr_closure(self, attr):
181
+ """
182
+ get the close of the given attribute
183
+
184
+ Args:
185
+ attr(str): the name of the attribute to calculate the closure for
186
+
187
+ Returns:
188
+ set: the closure of the attribute
189
+ """
190
+ # closure set is build up iteratively, until it does not expand anymore
191
+ closure = set(attr)
192
+ # set of previous iteration
193
+ last = set()
194
+ while closure != last:
195
+ last = closure.copy()
196
+ # check all FDs whether their initiators are part of the closure
197
+ # and add closure of the respective FD to the calculating closure
198
+ for dep in self.dependencies:
199
+ left, right = dep
200
+ if left and left.issubset(closure):
201
+ closure.update(right)
202
+ return closure
203
+
204
+ def attribute_combinations(self, attributeSet: Set):
205
+ """
206
+ generator for keys
207
+ """
208
+ for i in range(1, len(attributeSet) + 1):
209
+ for keys in combinations(attributeSet, i):
210
+ yield keys
211
+
212
+ def find_candidate_keys(self, verbose: bool = False, genEx: bool = False):
213
+ """
214
+ find candidate keys of the scheme
215
+
216
+ Args:
217
+ verbose(bool): if True show the steps
218
+ """
219
+ generatedTasks = []
220
+ ans = []
221
+ # check closures of all attributes and attribute combinations iteratively
222
+ # smaller candidate keys added first
223
+ for keys in self.attribute_combinations(self.attributes):
224
+ closure = self.get_attr_closure(keys)
225
+ k = set(keys)
226
+ if closure == self.attributes:
227
+ # no subset of currently checked key is already in
228
+ if not any([x.issubset(k) for x in ans]):
229
+ generatedTasks.append(
230
+ (
231
+ f"Es ist {k} ein Schlüsselkandidat. Also ist {self.attributes} voll funktional abhängig von {k}.",
232
+ True,
233
+ )
234
+ )
235
+ generatedTasks.append(
236
+ (
237
+ f"Die Attributhülle von {k} ist {self.attributes}. Aus diesem Grund ist {self.attributes} voll funktional abhängig von {k}.",
238
+ False,
239
+ )
240
+ )
241
+
242
+ if verbose:
243
+ print(f"found candidate keys {k}")
244
+ ans.append(k)
245
+ else:
246
+ generatedTasks.append(
247
+ (
248
+ f"Es ist {k} ein Superschlüssel, aber kein Schlüsselkandidat.",
249
+ True,
250
+ )
251
+ )
252
+ generatedTasks.append(
253
+ (
254
+ f"Es ist {k} ein Superschlüssel. Also ist {self.attributes} voll funktional abhängig von {k}.",
255
+ False,
256
+ )
257
+ )
258
+ else:
259
+ generatedTasks.append(
260
+ (f"Es ist {k} weder Superschlüssel noch Schlüsselkandidat.", True)
261
+ )
262
+ if len(k) <= 2:
263
+ generatedTasks.append((f"Aus {k} leitet man {closure} ab.", True))
264
+
265
+ if genEx:
266
+ print(f"=======================================")
267
+ print(
268
+ f"Aussagen zum Finden von Schlüsselkandidaten und Berechnen von Attributhüllen:"
269
+ )
270
+ for task in generatedTasks:
271
+ print(task)
272
+ print(f"=======================================")
273
+
274
+ return ans
275
+
276
+ def isFDinBCNF(self, fd):
277
+ """
278
+ test whether the given fd is in BCNF
279
+
280
+ Returns:
281
+ bool: True if the left side of the functional dependency is a superkey
282
+ """
283
+ left, right = fd
284
+ # do a super key check for the left side
285
+ leftIsSuperKey = self.isSuperKey(left) or (left >= right)
286
+ return leftIsSuperKey
287
+
288
+ def isSuperKey(self, attrSet):
289
+ """
290
+ check wether the given attribute Set is a superkey
291
+
292
+ Args:
293
+ attrSet(Set): the set of attributes to check
294
+
295
+ Returns:
296
+ bool: True if the attributeSet is a super key
297
+ """
298
+ closure_left = self.get_attr_closure(attrSet)
299
+ return closure_left == self.attributes
300
+
301
+ def isIdentical(self, fdsetToCompare):
302
+ """
303
+ tests whether a given second fdset has the same attributes and fds as self
304
+ """
305
+ # check whether attributes are same
306
+ if self.attributes != fdsetToCompare.attributes:
307
+ return False
308
+
309
+ # check whether all self dependencies are contained in compareFDSet dependencies
310
+ for selfPre, selfPost in self.dependencies:
311
+ found = False
312
+ for comparePre, comparePost in fdsetToCompare.dependencies:
313
+ if selfPre == comparePre and selfPost == comparePost:
314
+ found = True
315
+ break
316
+ if not found:
317
+ return False
318
+
319
+ # check whether all compareFDSet dependencies are contained in self dependencies
320
+ for comparePre, comparePost in self.dependencies:
321
+ found = False
322
+ for selfPre, selfPost in fdsetToCompare.dependencies:
323
+ if selfPre == comparePre and selfPost == comparePost:
324
+ found = True
325
+ break
326
+ if not found:
327
+ return False
328
+
329
+ return True
330
+
331
+ def isBCNF(self, verbose: bool = False):
332
+ """
333
+ tests whether i am in BCNF:
334
+ every left side of every dependency is a superkey
335
+ """
336
+ result = True
337
+ for fd in self.dependencies:
338
+ fdInBCNF = self.isFDinBCNF(fd)
339
+ result = result and fdInBCNF
340
+ if not result:
341
+ if verbose:
342
+ print(f"fd {fd} is NOT in BCNF")
343
+ return False
344
+ if verbose:
345
+ print(f"fd {fd} is in BCNF")
346
+
347
+ return result
348
+
349
+ def getNonBCNF(self):
350
+ """
351
+ get first non BCNF dependency from my dependencies
352
+ """
353
+ for fd in self.dependencies:
354
+ if not self.isFDinBCNF(fd):
355
+ return fd
356
+ return None
357
+
358
+ def decompose2(self, verbose: bool = False, genEx: bool = False):
359
+ """
360
+ decomposition algorithm according to DBIS-VL
361
+ Source: https://dbis.rwth-aachen.de/dbis-vl/RelDesign#page=82
362
+
363
+ Args:
364
+ verbose(bool): if True show steps
365
+ genEx(bool): if True generate exercise tasks in form of sentences that can be either true or false
366
+ """
367
+
368
+ # https://en.wikipedia.org/wiki/Linear_A version of Algorithm ...
369
+
370
+ generatedTasks = []
371
+ step = 0
372
+
373
+ self.isdecomposed = True
374
+ fds = FunctionalDependencySet()
375
+ fds.attributes = self.attributes.copy()
376
+ fds.dependencies = self.dependencies.copy()
377
+ fds.completeFDsetToClosure(verbose=verbose)
378
+ not_bcnf = [fds]
379
+ bcnf = []
380
+ while not_bcnf:
381
+ fds = not_bcnf.pop(0)
382
+ #
383
+ if fds.isBCNF(verbose=verbose):
384
+ generatedTasks.append(
385
+ (f"Die Abhängigkeitsmenge {fds} ist in BCNF.", True)
386
+ )
387
+ bcnf.append(fds.attributes)
388
+ if verbose:
389
+ print(f"fdset {fds} is in BCNF")
390
+ else:
391
+ generatedTasks.append(
392
+ (f"Die Abhängigkeitsmenge {fds} ist in BCNF.", False)
393
+ )
394
+ if verbose:
395
+ print(f"fdset {fds} is not yet in BCNF")
396
+ new_fds1 = FunctionalDependencySet()
397
+ new_fds2 = FunctionalDependencySet()
398
+ for dep in fds.dependencies:
399
+ left, right = dep
400
+ if (
401
+ (fds.get_attr_closure(left) != fds.attributes)
402
+ and (left != right)
403
+ and (len(left.intersection(right)) == 0)
404
+ ):
405
+ step += 1
406
+ generatedTasks.append(
407
+ (
408
+ f"In Schritt {step} zerlegen wir entlang der Abhängigkeit {dep}.",
409
+ True,
410
+ )
411
+ )
412
+ if verbose:
413
+ print(f"decompose along the dependency {dep}")
414
+
415
+ # create new fdsets along the dependency we decompose with
416
+ new_fds1.attributes = left | right
417
+ new_fds2.attributes = fds.attributes - right
418
+ # find dependencies that belong to the new fdsets
419
+ new_fds1.dependencies = [
420
+ fd
421
+ for fd in fds.dependencies
422
+ if (fd[0] | fd[1]) <= new_fds1.attributes
423
+ ]
424
+ new_fds2.dependencies = [
425
+ fd
426
+ for fd in fds.dependencies
427
+ if (fd[0] | fd[1]) <= new_fds2.attributes
428
+ ]
429
+ # new_fds2.dependencies = new_fds2.dependencies + [(fd[0], fd[1].intersection(new_fds2.attributes)) for fd in fds.dependencies if fd[0] <= new_fds2.attributes and len(fd[1].intersection(new_fds2.attributes)) != 0]
430
+ break
431
+ else:
432
+ generatedTasks.append(
433
+ (f"Wir zerlegen entlang der Abhängigkeit {dep}.", False)
434
+ )
435
+ not_bcnf.append(new_fds1)
436
+ not_bcnf.append(new_fds2)
437
+ if verbose:
438
+ print(
439
+ f"new fdsets with attributes {new_fds1.attributes} and {new_fds2.attributes}"
440
+ )
441
+ self.tables = bcnf
442
+
443
+ generatedTasks.append(
444
+ (f"Die Zerlegung enthält mehr als drei Relationen.", len(self.tables) > 3)
445
+ )
446
+
447
+ if genEx:
448
+ print(f"=======================================")
449
+ print(f"Aussagen zur Dekomposition:")
450
+ for task in generatedTasks:
451
+ print(task)
452
+ print(f"=======================================")
453
+
454
+ return bcnf
455
+
456
+ def decompose(self, verbose: bool = False):
457
+ """
458
+ decomposition algorithm
459
+
460
+ Args:
461
+ verbose(bool): if True show steps of the decomposition
462
+ """
463
+ self.isdecomposed = True
464
+ self.tables = [self.attributes]
465
+ for dep in self.dependencies:
466
+ left, right = dep
467
+ for attr_set in self.tables:
468
+ # newset contains the unity of attributes of the FD
469
+ newset = left.symmetric_difference(right)
470
+ # if newset is real subset, extra attributes still exist
471
+ # --> need to break it up
472
+ if newset.issubset(attr_set) and newset != attr_set:
473
+ if verbose:
474
+ print(
475
+ f"splitting {attr_set} into {attr_set.difference(right)} and {newset}"
476
+ )
477
+ # split attributes of the FD closure off the attribute set
478
+ attr_set.difference_update(right)
479
+
480
+ # add new BCNF set to list of attribute sets
481
+ self.tables.append(newset)
482
+ return self.tables
483
+
484
+ def decompose_all(self):
485
+ ## Messy sets and tuples to get rid of duplicates, eew
486
+ tables_possibilities = []
487
+
488
+ for ordering in permutations(self.dependencies):
489
+ tbl = [self.attributes.copy()]
490
+
491
+ for dep in ordering:
492
+ left, right = dep
493
+ for attr_set in tbl:
494
+ newset = left.symmetric_difference(right)
495
+ if newset.issubset(attr_set) and newset != attr_set:
496
+ attr_set.difference_update(right)
497
+ tbl.append(newset)
498
+
499
+ tbl = [tuple(x) for x in tbl]
500
+ tables_possibilities.append(tuple(tbl))
501
+
502
+ return set(tables_possibilities)
503
+
504
+ def is_lossy(self):
505
+ """
506
+ check for lossyness
507
+
508
+ Returns:
509
+ bool: True if if one of my dependencies is not preserved
510
+ """
511
+ if not self.isdecomposed:
512
+ raise Exception("Can't tell if lossy if the FD hasn't been decomposed yet")
513
+ for dep in self.dependencies:
514
+ if not self.is_preserved(dep):
515
+ return True
516
+ return False
517
+
518
+ def is_preserved(self, dep):
519
+ """
520
+ check whether the given dependency is preserved
521
+
522
+ Args:
523
+ dep(): the dependency to check
524
+
525
+ Returns:
526
+ bool: True if the dependency is preserved
527
+ """
528
+ left, right = dep
529
+ pre = left.symmetric_difference(right)
530
+ for attr_set in self.tables:
531
+ if pre == attr_set:
532
+ return True
533
+ return False
534
+
535
+ def calculate_fds_in_subset(self, subset):
536
+ """
537
+ calculate all dependencies in a subset. Also includes dependencies for which
538
+ attribute parts are missing because they are not in the subset. Does not include
539
+ original dependencies that have lost all there attributes in precondition or closure
540
+
541
+ """
542
+ subset_dependencies = []
543
+ for dep in self.dependencies:
544
+ new_dep_pre = set()
545
+ new_dep_post = set()
546
+ left, right = dep
547
+ # check whether attributes occur in pre or post of the original FD
548
+ for attr in left:
549
+ if attr in subset:
550
+ new_dep_pre.add(attr)
551
+ for attr in right:
552
+ if attr in subset:
553
+ new_dep_post.add(attr)
554
+ # only add new dependency if none of both sides is empty
555
+ if new_dep_pre != set() and new_dep_post != set():
556
+ subset_dependencies.append((new_dep_pre, new_dep_post))
557
+ return subset_dependencies
558
+
559
+ def is2NF(self):
560
+ """
561
+ calculates whether the FD set is in 2NF: Every attribute has to depend on the whole CK.
562
+ Check for every attribute whether there is a part of any of the CKs which has the attribute in its closure
563
+ """
564
+ ckeys = self.find_candidate_keys()
565
+ # check every non-ck-attribute
566
+ for attr in self.attributes:
567
+ skip = False
568
+ for ckey in ckeys:
569
+ for ckey_part in ckey:
570
+ if attr == ckey_part:
571
+ skip = True
572
+
573
+ if skip == True:
574
+ continue
575
+
576
+ # check every key candidate
577
+ for ckey in ckeys:
578
+ # check every subset of keys (not yet)
579
+ for ckey_part in ckey:
580
+ ckey_part_closure = self.get_attr_closure(ckey_part)
581
+ if attr in ckey_part_closure:
582
+ return False
583
+ return True
584
+
585
+ def is3NF(self):
586
+ """
587
+ calculates whether the FD set is in 3NF: There are no dependencies between non-key attributes
588
+ """
589
+ ckeys = self.find_candidate_keys()
590
+
591
+ for dep in self.dependencies:
592
+ left, right = dep
593
+ # get all attributes of an fd
594
+ dep_attributes = set()
595
+ dep_attributes.update(left)
596
+ dep_attributes.update(right)
597
+ dep_has_ckey_attr = False
598
+
599
+ # check all attributes of the fd whether at least one of them is contained in a ckey
600
+ for attr in dep_attributes:
601
+ for ckey in ckeys:
602
+ if set(attr).issubset(ckey):
603
+ dep_has_ckey_attr = True
604
+ break
605
+ if not dep_has_ckey_attr:
606
+ return False
607
+ return True
608
+
609
+ def generate_cluster(self, shape: str = "box", indent: str = " "):
610
+ """
611
+ graphviz digraph subgraph (cluster) generation for this functional dependency set
612
+
613
+ Args:
614
+ shape(str): the shape to use - default: box
615
+ indent(str): indentation - default: two spaces
616
+ Return:
617
+ str: graphviz markup
618
+ """
619
+ markup = ""
620
+ # sort dependencies by largest pre
621
+ dependencies = self.dependencies.copy()
622
+ dependencies.sort(key=lambda dep: len(dep[0]), reverse=True)
623
+
624
+ # collect attributes that are only on the right side
625
+ only_post = self.attributes.copy()
626
+ # generate clusters
627
+ cluster_markup = ""
628
+ for dep in dependencies:
629
+ pre, post = dep
630
+ only_post -= pre
631
+ cluster_name = "".join(sorted(pre))
632
+ cluster_markup += f"{indent}subgraph cluster_{cluster_name}{{\n"
633
+ cluster_markup += f'{indent} label="{cluster_name}"\n'
634
+ for attrVar in sorted(pre):
635
+ attr = self.attribute_map[attrVar]
636
+ cluster_markup += (
637
+ f'{indent}{indent}{attrVar} [shape={shape} label="{attr}"]\n'
638
+ )
639
+ cluster_markup += f"{indent}}}\n"
640
+
641
+ # generate arrows
642
+ arrow_markup = ""
643
+ for dep in dependencies:
644
+ pre, post = dep
645
+ for attrVar in sorted(post):
646
+ arrow_markup += f"{indent}{sorted(pre)[0]}->{attrVar}\n"
647
+
648
+ # create markup for only post attributes
649
+ only_post_markup = ""
650
+ for attrVar in sorted(only_post):
651
+ attr = self.attribute_map[attrVar]
652
+ only_post_markup += f'{indent}{attrVar} [shape={shape} label="{attr}"]\n'
653
+
654
+ # concatenate markup
655
+ markup += only_post_markup
656
+ markup += cluster_markup
657
+ markup += arrow_markup
658
+ return markup
659
+
660
+ def as_graphviz(self, withCluster: bool = True):
661
+ """
662
+
663
+ convert me to a graphviz markup e.g. to try out in
664
+
665
+ http://magjac.com/graphviz-visual-editor/
666
+ or
667
+ http://diagrams.bitplan.com
668
+
669
+ Return:
670
+ str: the graphviz markup for this functional DependencySet
671
+ """
672
+ markup = f"#generated by {__file__} on {self.isodate}\n"
673
+ markup += "digraph functionalDependencySet{"
674
+ # add title see https://stackoverflow.com/a/6452088/1497139
675
+ markup += f"""
676
+ // title
677
+ labelloc="t";
678
+ label="{self.title}"
679
+ """
680
+ if not withCluster:
681
+ markup += "// Attribute variables \n"
682
+ for attrVar in sorted(self.attributes):
683
+ attr = self.attribute_map[attrVar]
684
+ markup += f""" {attrVar} [ shape=box label="{attr}"] \n"""
685
+ else:
686
+ markup += self.generate_cluster()
687
+ markup += "}"
688
+ return markup
689
+
690
+ def left_reduction(self, verbose: bool = False, genEx: bool = False):
691
+ """
692
+ executes a left reduction on the dependencies from this fdset
693
+
694
+ Args:
695
+ verbose(bool): if True show steps
696
+ genEx(bool): if True generate exercise tasks in form of sentences that can be either true or false
697
+ """
698
+
699
+ generatedTasks = []
700
+ reductionCounter = 0
701
+
702
+ if genEx:
703
+ self.notation = Notation.math
704
+
705
+ remaining_deps = self.dependencies.copy()
706
+ while remaining_deps:
707
+ dep = remaining_deps.pop(0)
708
+ pre, post = dep
709
+ wasDepAlreadyReduced = False
710
+ for attr in sorted(pre):
711
+ if post <= self.get_attr_closure(pre - {attr}):
712
+ generatedTasks.append(
713
+ (
714
+ f"Das Attribut {attr} kann aus der Abhängigkeit {dep} reduziert werden.",
715
+ True,
716
+ )
717
+ )
718
+ if not wasDepAlreadyReduced:
719
+ generatedTasks.append(
720
+ (
721
+ f"Die Abhängigkeit {self.stringify_dependencies(fdsToStringify=[dep])} kann nicht reduziert werden, da die Attributhülle von {Set.stringify_set(post, self.notation)} nicht alle Attribute aus {Set.stringify_set(pre, self.notation)} enthält.",
722
+ False,
723
+ )
724
+ )
725
+ generatedTasks.append(
726
+ (
727
+ f"Die Abhängigkeit {self.stringify_dependencies(fdsToStringify=[dep])} kann reduziert werden, da die Attributhülle von {Set.stringify_set(post, self.notation)} nicht alle Attribute aus {Set.stringify_set(pre, self.notation)} enthält.",
728
+ False,
729
+ )
730
+ )
731
+ reductionCounter += 1
732
+ else:
733
+ generatedTasks.append(
734
+ (
735
+ f"Die Abhängigkeit {self.stringify_dependencies(fdsToStringify=[dep])} wird mehrmals reduziert.",
736
+ True,
737
+ )
738
+ )
739
+ if verbose:
740
+ print(
741
+ f"removed {attr} from lhs of dependency {dep}, new lhs {(pre - {attr})}"
742
+ )
743
+ self.remove_dependency(pre, post)
744
+ self.add_dependency(pre - {attr}, post)
745
+ pre = pre - {attr}
746
+ wasDepAlreadyReduced = True
747
+ else:
748
+ generatedTasks.append(
749
+ (
750
+ f"Das Attribut {attr} kann aus der Abhängigkeit {self.stringify_dependencies(fdsToStringify=[dep])} reduziert werden.",
751
+ False,
752
+ )
753
+ )
754
+ if not wasDepAlreadyReduced:
755
+ generatedTasks.append(
756
+ (
757
+ f"Die Abhängigkeit {self.stringify_dependencies(fdsToStringify=[dep])} kann nicht reduziert werden.",
758
+ True,
759
+ )
760
+ )
761
+ else:
762
+ generatedTasks.append(
763
+ (
764
+ f"Die Linksreduktion beinhaltet die Abhängigkeit {self.stringify_dependencies(fdsToStringify=[dep])}.",
765
+ True,
766
+ )
767
+ )
768
+
769
+ generatedTasks.append(
770
+ (
771
+ f"Es kann nur eine der ursprünglichen funktionalen Abhängigkeiten reduziert werden.",
772
+ reductionCounter == 1,
773
+ )
774
+ )
775
+ generatedTasks.append(
776
+ (
777
+ f"Es können mehr als zwei der ursprünglichen funktionalen Abhängigkeiten reduziert werden.",
778
+ reductionCounter > 2,
779
+ )
780
+ )
781
+
782
+ if genEx:
783
+ print(f"=======================================")
784
+ print(f"Aussagen zur Linksreduktion (Schritt 1a):")
785
+ for task in generatedTasks:
786
+ print(task)
787
+ print(f"=======================================")
788
+
789
+ def isCorrectLeftReduction(self, proposedReduction, verbose: bool = False):
790
+ """
791
+ Checks a given left reduction of self for correctness
792
+
793
+ Args:
794
+ proposedReduction(FunctionalDependencySet): The proposed left reduction of self, the original FDSet
795
+ verbose(bool): Optional argument for detailed output
796
+ Return:
797
+ bool: Is the left reduction correct
798
+ """
799
+ if verbose:
800
+ print("CHECKING: Is the proposed left reduction of self correct:\n")
801
+ print("1. Is the amount of dependencies of the solution the same as self:")
802
+
803
+ # equal amount of dependencies
804
+ if len(proposedReduction.dependencies) != len(self.dependencies):
805
+ if verbose:
806
+ print(
807
+ f"FAILURE: The solution had a differing amount of dependencies: \n \t Task: {len(self.dependencies)}\n\tSolution: {len(proposedReduction.dependencies)}"
808
+ )
809
+ return False
810
+ if verbose:
811
+ print(
812
+ f"Both the task and the solution have {len(self.dependencies)} dependencies\n"
813
+ )
814
+ print(
815
+ f"2. Checking whether the solution dependencies could result from a task dependency:"
816
+ )
817
+
818
+ # equal right sides of dependencies
819
+ # left sides are subsets of original left sides
820
+ for propDep in proposedReduction.dependencies:
821
+ leftProp, rightProp = propDep
822
+ if not leftProp:
823
+ if verbose:
824
+ print(
825
+ f"FAIL: One of the dependencies has an empty left side. This cannot happen."
826
+ )
827
+ return False
828
+ isCorrect = False
829
+ for oriDep in self.dependencies:
830
+ leftOri, rightOri = oriDep
831
+ if verbose:
832
+ print(
833
+ f"\tComparing solution dependency {leftProp} -> {rightProp} with task dependency {leftOri} -> {rightOri}"
834
+ )
835
+ if rightProp == rightOri and leftProp <= leftOri:
836
+ isCorrect = True
837
+ if verbose:
838
+ print(
839
+ f"\tThe solution dependency can result from the task dependency"
840
+ )
841
+ break
842
+ if not isCorrect:
843
+ if verbose:
844
+ print(
845
+ f"FAILURE: The dependency {leftProp} -> {rightProp} can't result from any task dependency"
846
+ )
847
+ return False
848
+ if verbose:
849
+ print(f"All solution dependencies can result from the task dependencies\n")
850
+ print(
851
+ f"3. Checking whether the closures of all attribute subsets have remained the same:"
852
+ )
853
+
854
+ # check whether attribute closures of all attribute subsets are still the same
855
+ if not self.isAllAttributeClosuresSame(proposedReduction, verbose):
856
+ return False
857
+ if verbose:
858
+ print(f"4. Checking whether the proposed reduction is complete:")
859
+
860
+ # reduction is complete: reduction of the reduction remains the same
861
+ doubleReduction = proposedReduction.copy()
862
+ doubleReduction.left_reduction()
863
+ if not doubleReduction.isIdentical(proposedReduction):
864
+ if verbose:
865
+ print(f"FAILURE: The proposed reduction is not complete")
866
+ return False
867
+ if verbose:
868
+ print(f"The proposed reduction is complete\n")
869
+ print(f"SUCCESS: The left reduction is correct")
870
+
871
+ return True
872
+
873
+ def isAllAttributeClosuresSame(self, fdsetToCompare, verbose: bool = False):
874
+ """
875
+ check whether attribute closures of all attribute subsets are still the same
876
+ """
877
+ combs = list(self.attribute_combinations(self.attributes))
878
+ for comb in combs:
879
+ if verbose:
880
+ print(f"\tChecking set of attributes: {set(comb)}")
881
+ if self.get_attr_closure(comb) != fdsetToCompare.get_attr_closure(comb):
882
+ if verbose:
883
+ print(
884
+ f"FAILURE: The closure for the subset {set(comb)} changed from {self.get_attr_closure(comb)} to {fdsetToCompare.get_attr_closure(comb)}"
885
+ )
886
+ return False
887
+ if verbose:
888
+ print(f"All subsets of attributes have the same closure\n")
889
+
890
+ return True
891
+
892
+ def completeFDsetToClosure(self, verbose: bool = False):
893
+ """
894
+ generates the closure F^+ of an FD set F
895
+ """
896
+ combs = list(self.attribute_combinations(self.attributes))
897
+ for comb in combs:
898
+ if verbose:
899
+ print(f"\tChecking set of attributes: {set(comb)}")
900
+ closure = self.get_attr_closure(comb)
901
+ if verbose:
902
+ print(f"Closure of {set(comb)} is {closure}")
903
+ rhs_combs = list(self.attribute_combinations(closure))
904
+ for rhs_comb in rhs_combs:
905
+ self.dependencies.append((set(comb), set(rhs_comb)))
906
+ if verbose:
907
+ print(f"adding dependency {set(comb)}, {set(rhs_comb)}")
908
+ return
909
+
910
+ def right_reduction(self, verbose: bool = False, genEx: bool = False):
911
+ """
912
+ executes a right reduction on the dependencies from this fdset
913
+
914
+ Args:
915
+ verbose(bool): if True show steps
916
+ genEx(bool): if True generate exercise tasks in form of sentences that can be either true or false
917
+ """
918
+ if genEx:
919
+ self.notation = Notation.math
920
+
921
+ generatedTasks = []
922
+ reductionCounter = 0
923
+ remaining_deps = self.dependencies.copy()
924
+ while remaining_deps:
925
+ dep = remaining_deps.pop(0)
926
+ pre, post = dep
927
+ wasDepAlreadyReduced = False
928
+ for attr in sorted(post):
929
+ self.remove_dependency(pre, post)
930
+ self.add_dependency(pre, post - {attr})
931
+ if {attr} <= set(self.get_attr_closure(pre)):
932
+ if not wasDepAlreadyReduced:
933
+ generatedTasks.append(
934
+ (
935
+ f"Die Abhängigkeit {self.stringify_dependencies(fdsToStringify=[dep])} kann reduziert werden, da die Attributhülle von {post} nicht alle Attribute aus {pre} enthält.",
936
+ False,
937
+ )
938
+ )
939
+ generatedTasks.append(
940
+ (
941
+ f"Das Attribut {attr} kann aus der Abhängigkeit {self.stringify_dependencies(fdsToStringify=[dep])} reduziert werden.",
942
+ True,
943
+ )
944
+ )
945
+ generatedTasks.append(
946
+ (
947
+ f"Das Attribut {attr} kann aus der Abhängigkeit {self.stringify_dependencies(fdsToStringify=[dep])} reduziert werden, da bei Entfernen von {attr} von der rechten Seite der Abhängigkeit die Attributhülle von {pre} immer noch {attr} enthält.",
948
+ True,
949
+ )
950
+ )
951
+ generatedTasks.append(
952
+ (
953
+ f"Das Attribut {attr} kann aus der Abhängigkeit {self.stringify_dependencies(fdsToStringify=[dep])} reduziert werden, da bei Entfernen von {attr} von der rechten Seite der Abhängigkeit die Attributhülle von {post - {attr}} immer noch {attr} enthält.",
954
+ True,
955
+ )
956
+ )
957
+ reductionCounter += 1
958
+ else:
959
+ generatedTasks.append(
960
+ (
961
+ f"Die Abhängigkeit {self.stringify_dependencies(fdsToStringify=[dep])} kann mehrmals reduziert werden.",
962
+ True,
963
+ )
964
+ )
965
+ if verbose:
966
+ print(
967
+ f"removed {attr} from rhs of dependency {dep}, new rhs {(post - {attr})}"
968
+ )
969
+ post = post - {attr}
970
+ wasDepAlreadyReduced = True
971
+ else:
972
+ generatedTasks.append(
973
+ (
974
+ f"Das Attribut {attr} kann aus der Abhängigkeit {self.stringify_dependencies(fdsToStringify=[dep])} reduziert werden.",
975
+ False,
976
+ )
977
+ )
978
+ self.remove_dependency(pre, post - {attr})
979
+ self.add_dependency(pre, post)
980
+ if wasDepAlreadyReduced:
981
+ generatedTasks.append(
982
+ (
983
+ f"Die Abhängigkeit {self.stringify_dependencies(fdsToStringify=[dep])} wird insgesamt zu {self.stringify_dependencies(fdsToStringify=[(pre, post)])} reduziert.",
984
+ True,
985
+ )
986
+ )
987
+ else:
988
+ generatedTasks.append(
989
+ (
990
+ f"Die Abhängigkeit {self.stringify_dependencies(fdsToStringify=[dep])} kann nicht reduziert werden.",
991
+ True,
992
+ )
993
+ )
994
+
995
+ generatedTasks.append(
996
+ (
997
+ f"Es kann nur eine der ursprünglichen funktionalen Abhängigkeiten reduziert werden.",
998
+ reductionCounter == 1,
999
+ )
1000
+ )
1001
+ generatedTasks.append(
1002
+ (
1003
+ f"Es können mehr als zwei der ursprünglichen funktionalen Abhängigkeiten reduziert werden.",
1004
+ reductionCounter > 2,
1005
+ )
1006
+ )
1007
+
1008
+ if genEx:
1009
+ print(f"=======================================")
1010
+ print(f"Aussagen zur Rechtsreduktion (Schritt 1b):")
1011
+ for task in generatedTasks:
1012
+ print(task)
1013
+ print(f"=======================================")
1014
+
1015
+ def isCorrectRightReduction(self, proposedReduction, verbose: bool = False):
1016
+ """
1017
+ Checks a given right reduction of self for correctness
1018
+
1019
+ Args:
1020
+ self: an FDSet after left reduction, but before right reduction
1021
+ proposedReduction(FunctionalDependencySet): The proposed right reduction of self
1022
+ verbose(bool): Optional argument for detailed output
1023
+ Return:
1024
+ bool: Is the right reduction correct
1025
+ """
1026
+ if verbose:
1027
+ print("CHECKING: Is the proposed right reduction of self correct:\n")
1028
+ print("1. Is the amount of dependencies of the solution the same as self:")
1029
+
1030
+ # equal amount of dependencies
1031
+ if len(proposedReduction.dependencies) != len(self.dependencies):
1032
+ if verbose:
1033
+ print(
1034
+ f"FAILURE: The solution had a differing amount of dependencies: \n \t Task: {len(self.dependencies)}\n\tSolution: {len(proposedReduction.dependencies)}"
1035
+ )
1036
+ return False
1037
+ if verbose:
1038
+ print(
1039
+ f"Both the task and the solution have {len(self.dependencies)} dependencies\n"
1040
+ )
1041
+ print(
1042
+ f"2. Checking whether the solution dependencies could result from a task dependency:"
1043
+ )
1044
+
1045
+ # equal left sides of dependencies
1046
+ # right sides are subsets of original right sides
1047
+ for propDep in proposedReduction.dependencies:
1048
+ leftProp, rightProp = propDep
1049
+ if not leftProp:
1050
+ if verbose:
1051
+ print(
1052
+ f"FAIL: One of the dependencies has an empty left side. This cannot happen."
1053
+ )
1054
+ return False
1055
+ isCorrect = False
1056
+ for oriDep in self.dependencies:
1057
+ leftOri, rightOri = oriDep
1058
+ if verbose:
1059
+ print(
1060
+ f"\tComparing solution dependency {leftProp} -> {rightProp} with task dependency {leftOri} -> {rightOri}"
1061
+ )
1062
+ if leftProp == leftOri and rightProp <= rightOri:
1063
+ isCorrect = True
1064
+ if verbose:
1065
+ print(
1066
+ f"\tThe solution dependency can result from the task dependency"
1067
+ )
1068
+ break
1069
+ if not isCorrect:
1070
+ if verbose:
1071
+ print(
1072
+ f"FAILURE: The dependency {leftProp} -> {rightProp} can't result from any task dependency"
1073
+ )
1074
+ return False
1075
+ if verbose:
1076
+ print(f"All solution dependencies can result from the task dependencies\n")
1077
+ print(
1078
+ f"3. Checking whether the closures of all attribute subsets have remained the same:"
1079
+ )
1080
+
1081
+ # check whether attribute closures of all attribute subsets are still the same
1082
+ if not self.isAllAttributeClosuresSame(proposedReduction, verbose):
1083
+ return False
1084
+ if verbose:
1085
+ print(f"4. Checking whether the proposed reduction is complete:")
1086
+
1087
+ # reduction is complete: reduction of the reduction remains the same
1088
+ doubleReduction = proposedReduction.copy()
1089
+ doubleReduction.right_reduction()
1090
+ if not doubleReduction.isIdentical(proposedReduction):
1091
+ if verbose:
1092
+ print(f"FAILURE: The proposed reduction is not complete")
1093
+ return False
1094
+ if verbose:
1095
+ print(f"The proposed reduction is complete\n")
1096
+ print(f"SUCCESS: The right reduction is correct")
1097
+
1098
+ return True
1099
+
1100
+ def remove_empty_fds(self, verbose: bool = False, genEx: bool = False):
1101
+ """
1102
+ remove empty fds of form "A → {}" from this fdset
1103
+
1104
+ Args:
1105
+ verbose(bool): if True show steps
1106
+ genEx(bool): if True generate exercise tasks in form of sentences that can be either true or false
1107
+ """
1108
+ removeCounter = 0
1109
+ deps_copy = self.dependencies.copy()
1110
+ for dep in deps_copy:
1111
+ pre, post = dep
1112
+ if len(post) == 0:
1113
+ removeCounter += 1
1114
+ if verbose:
1115
+ print(f"removed {dep} because rhs is empty")
1116
+ self.remove_dependency(pre, post)
1117
+
1118
+ if genEx:
1119
+ print(f"=======================================")
1120
+ print(f"Aussagen zum Entfernen leerer FDs (Schritt 1c):")
1121
+ print(
1122
+ (
1123
+ f"Es werden zwei Abhängigkeiten entfernt, da sie eine leere rechte Seite haben.",
1124
+ removeCounter == 2,
1125
+ )
1126
+ )
1127
+ print(
1128
+ (
1129
+ f"Es werden zwei Abhängigkeiten entfernt, da sie eine leere linke Seite haben.",
1130
+ False,
1131
+ )
1132
+ )
1133
+ print(
1134
+ (f"Es werden mehr als zwei Abhängigkeiten entfernt.", removeCounter > 2)
1135
+ )
1136
+ print(f"=======================================")
1137
+
1138
+ def isCorrectRemovingEmptyFDs(self, proposedSolution, verbose: bool = False):
1139
+ """
1140
+ Checks a given removing of empty fds of self for correctness
1141
+
1142
+ Args:
1143
+ self: an FDSet after removing empty fds
1144
+ proposedSolution(FunctionalDependencySet): The proposed solution of removing fds of self
1145
+ verbose(bool): Optional argument for detailed output
1146
+ Return:
1147
+ bool: Is the solution correct
1148
+ """
1149
+ if verbose:
1150
+ print(
1151
+ "CHECKING: Is the proposed removal of empty dependencies of self correct:\n"
1152
+ )
1153
+ print(
1154
+ "1. Does the solution have an equal or less amount of dependencies than self:"
1155
+ )
1156
+
1157
+ # Equal amount of dependencies
1158
+ if len(proposedSolution.dependencies) <= len(self.dependencies):
1159
+ if verbose:
1160
+ print(
1161
+ f"FAILURE: The solution had more dependencies: \n \t Task: {len(self.dependencies)}\n\tSolution: {len(proposedSolution.dependencies)}"
1162
+ )
1163
+ if verbose:
1164
+ print(
1165
+ f"The proposed solution has equal or less dependencies ({len(proposedSolution.dependencies)}) than self ({len(self.dependencies)})\n"
1166
+ )
1167
+ print(
1168
+ f"2. Checking if every complete dependency in self is in the proposed solution:"
1169
+ )
1170
+
1171
+ # check if the correct dependencies remain
1172
+ for dep in self.dependencies:
1173
+ left, right = dep
1174
+ if right != Set() and (left, right) not in proposedSolution.dependencies:
1175
+ if verbose:
1176
+ print(
1177
+ f"FAILURE: Dependency {left} -> {right} is not in the proposed solution."
1178
+ )
1179
+ return False
1180
+ elif verbose:
1181
+ print(f"\tThe dependency {left} -> {right} is in the proposed solution")
1182
+ if verbose:
1183
+ print(f"All complete dependencies of self are in the proposed solution.\n")
1184
+ print(f"3. Check if all empty dependencies are removed:")
1185
+
1186
+ fds = self.copy()
1187
+ fds.remove_empty_fds()
1188
+ if not fds.isIdentical(proposedSolution):
1189
+ if verbose:
1190
+ print(f"FAILURE: Not all empty dependencies were removed")
1191
+ return False
1192
+
1193
+ if verbose:
1194
+ print(f"All empty dependencies have been removed.")
1195
+ print(f"SUCCESS: The proposed solution is correct.")
1196
+
1197
+ return True
1198
+
1199
+ def combine_fds(self, verbose: bool = False, genEx: bool = False):
1200
+ """
1201
+ combines fds with equal left sides
1202
+
1203
+ Args:
1204
+ verbose(bool): if True show steps
1205
+ genEx(bool): if True generate exercise tasks in form of sentences that can be either true or false
1206
+ """
1207
+ if genEx:
1208
+ self.notation = Notation.math
1209
+
1210
+ generatedTasks = []
1211
+ combined_dependencies = []
1212
+ combineCounter = 0
1213
+ deps_copy = self.dependencies.copy()
1214
+ while self.dependencies:
1215
+ pre, post = self.dependencies.pop(0)
1216
+ new_post = post
1217
+ deps_copy = self.dependencies.copy()
1218
+ for dep in deps_copy:
1219
+ left, right = dep
1220
+ if left == pre:
1221
+ combineCounter += 1
1222
+ new_post = new_post | right
1223
+ if verbose:
1224
+ print(
1225
+ f"combined dependencies {dep} and {(pre, post)} to new dependency {(pre, new_post)}"
1226
+ )
1227
+ self.remove_dependency(left, right)
1228
+ generatedTasks.append(
1229
+ (
1230
+ f"Die Abhängigkeiten {self.stringify_dependencies(fdsToStringify=[dep])} und {self.stringify_dependencies(fdsToStringify=[(pre, post)])} werden zu {self.stringify_dependencies(fdsToStringify=[(pre, new_post)])} zusammengefasst.",
1231
+ True,
1232
+ )
1233
+ )
1234
+ generatedTasks.append(
1235
+ (
1236
+ f"Die Abhängigkeiten {self.stringify_dependencies(fdsToStringify=[dep])} und {self.stringify_dependencies(fdsToStringify=[(pre, new_post)])} werden zu {self.stringify_dependencies(fdsToStringify=[(pre, post)])} zusammengefasst.",
1237
+ False,
1238
+ )
1239
+ )
1240
+ combined_dependencies.append((pre, new_post))
1241
+ self.dependencies = combined_dependencies
1242
+ generatedTasks.append(
1243
+ (
1244
+ f"Es werden an zwei oder mehr Stellen Abhängigkeiten zusammengefasst.",
1245
+ combineCounter >= 2,
1246
+ )
1247
+ )
1248
+
1249
+ for dep in self.dependencies:
1250
+ pre, post = dep
1251
+ generatedTasks.append(
1252
+ (
1253
+ f"Die kanonische Überdeckung enthält die funktionale Abhängigkeit {self.stringify_dependencies(fdsToStringify=[dep])}.",
1254
+ True,
1255
+ )
1256
+ )
1257
+ for attr in post:
1258
+ generatedTasks.append(
1259
+ (
1260
+ f"Die kanonische Überdeckung enthält die funktionale Abhängigkeit {self.stringify_dependencies(fdsToStringify=[(pre, post - {attr})])}.",
1261
+ False,
1262
+ )
1263
+ )
1264
+
1265
+ if genEx:
1266
+ print(f"=======================================")
1267
+ print(f"Aussagen zum Zusammenfassen von FDs (Schritt 1d):")
1268
+ for task in generatedTasks:
1269
+ print(task)
1270
+ print(f"=======================================")
1271
+
1272
+ def getFDsByLeftSide(self, left: Set, dependencies: list):
1273
+ filtered = list(filter(lambda dependecy: dependecy[0] == left, dependencies))
1274
+ return filtered
1275
+
1276
+ def isCorrectCombinationOfDependencies(
1277
+ self, proposedSolution, verbose: bool = False
1278
+ ):
1279
+ """
1280
+ Checks a given removing of empty fds of self for correctness
1281
+
1282
+ Args:
1283
+ self: an FDSet after combination of dependencies
1284
+ proposedSolution(FunctionalDependencySet): The proposed solution of combination of dependencies of self
1285
+ verbose(bool): Optional argument for detailed output
1286
+ Return:
1287
+ bool: Is the solution correct
1288
+ """
1289
+ if verbose:
1290
+ print(
1291
+ "CHECKING: Is the proposed combination of dependencies of self correct:\n"
1292
+ )
1293
+ print(
1294
+ "1. Does the solution have an equal or less amount of dependencies than self:"
1295
+ )
1296
+
1297
+ # Equal amount of dependencies
1298
+ if len(proposedSolution.dependencies) <= len(self.dependencies):
1299
+ if verbose:
1300
+ print(
1301
+ f"FAILURE: The solution had more dependencies: \n\tTask: {len(self.dependencies)}\n\tSolution: {len(proposedSolution.dependencies)}"
1302
+ )
1303
+ if verbose:
1304
+ print(
1305
+ f"The proposed solution has equal or less dependencies ({len(proposedSolution.dependencies)}) than self ({len(self.dependencies)})\n"
1306
+ )
1307
+ print(
1308
+ f"2. Checking if every combined dependency in self is in the proposed solution:"
1309
+ )
1310
+
1311
+ # check if the correct dependencies remain
1312
+ dependencies = self.dependencies.copy()
1313
+ for dep in proposedSolution.dependencies:
1314
+ left, originalRight = dep
1315
+ right = originalRight.copy()
1316
+ leftDeps = self.getFDsByLeftSide(left, dependencies)
1317
+ for leftDep in leftDeps:
1318
+ if leftDep[1] <= originalRight:
1319
+ dependencies.remove(leftDep)
1320
+ right -= leftDep[1]
1321
+ else:
1322
+ if verbose:
1323
+ print(
1324
+ f"FAILURE: Dependecy {left} -> {leftDep[1]} is not part of the combined dependency {left} -> {right}."
1325
+ )
1326
+ return False
1327
+ if verbose:
1328
+ print(
1329
+ f"\tThe dependency {left} -> {right} cannot be combined further."
1330
+ )
1331
+ if len(right) > 0:
1332
+ if verbose:
1333
+ print(
1334
+ f"FAILURE: Right side {dep[1]} contains too many attributes {right}."
1335
+ )
1336
+ return False
1337
+ if len(dependencies) > 0:
1338
+ if verbose:
1339
+ print("FAILURE: Some dependencies have not made it into the solution.")
1340
+ return False
1341
+ if verbose:
1342
+ print(f"SUCCESS: The proposed solution is correct.")
1343
+ return True
1344
+
1345
+ def canonical_cover(self, verbose: bool = False, genEx: bool = False):
1346
+ """
1347
+ determines the canonical cover of this fdset
1348
+
1349
+ 4 substeps with respective functions
1350
+
1351
+ https://git.rwth-aachen.de/i5/teaching/dbis-vl/-/raw/main/6-RelDesign/6-RelationaleEntwurfstheorie.pdf#page=39
1352
+
1353
+ Args:
1354
+ verbose(bool): if True show steps
1355
+ genEx(bool): if True generate exercise tasks in form of sentences that can be either true or false
1356
+ """
1357
+ self.left_reduction(verbose, genEx)
1358
+ self.right_reduction(verbose, genEx)
1359
+ if verbose:
1360
+ print(f"after right reduction:")
1361
+ for fd in self.dependencies:
1362
+ print(fd)
1363
+ self.remove_empty_fds(verbose, genEx)
1364
+ self.combine_fds(verbose, genEx)
1365
+ if verbose:
1366
+ print(f"canonical cover:")
1367
+ for fd in self.dependencies:
1368
+ print(fd)
1369
+
1370
+ def isCorrectCanonicalCover(self, proposedSolution, verbose: bool = False):
1371
+ """
1372
+ Checks if a give FunctionalDependencySet is a canonical cover of self
1373
+
1374
+ Args:
1375
+ proposedSolution(FunctionalDependencySet): The proposed canoncial cover of self
1376
+ verbose(bool): if True shows steps for debugging
1377
+ Return:
1378
+ bool: Correct?
1379
+ """
1380
+ if verbose:
1381
+ print("CHECKING: Is the proposed canonical cover of self correct:\n")
1382
+ print("1. Do self and proposedSolution have the same closures:")
1383
+ if not proposedSolution.isAllAttributeClosuresSame(self, verbose=verbose):
1384
+ if verbose:
1385
+ print(
1386
+ "FAILURE: The closures of self and proposedSolution are different."
1387
+ )
1388
+ if verbose:
1389
+ print("2. Checking if the canonical cover is complete:")
1390
+
1391
+ doubleCover = proposedSolution.copy()
1392
+ doubleCover.canonical_cover(verbose=verbose)
1393
+ if proposedSolution.isIdentical(doubleCover):
1394
+ if verbose:
1395
+ print("SUCCESS: The proposedSolution is correct")
1396
+ return True
1397
+
1398
+ if verbose:
1399
+ print("FAILURE: The proposedSolution is an incomplete canonical cover")
1400
+ return False
1401
+
1402
+ def create_new_fdsets(self, verbose: bool = False, genEx: bool = False):
1403
+ """
1404
+ create fdsets from the dependencies resulting from the canonical cover
1405
+
1406
+ Args:
1407
+ verbose(bool): if True show steps
1408
+ genEx(bool): if True generate exercise tasks in form of sentences that can be either true or false
1409
+
1410
+ Return:
1411
+ list[FunctionalDependencySet]: list of fdsets created from the dependencies resulting from the canonical cover
1412
+ """
1413
+ if genEx:
1414
+ self.notation = Notation.math
1415
+
1416
+ deps = self.dependencies.copy()
1417
+ existsDepWithMultipleFDs = False
1418
+ generatedTasks = []
1419
+ i = 1
1420
+ new_fdsets = []
1421
+ while deps:
1422
+ tmp = deps.pop(0)
1423
+ pre, post = tmp
1424
+ new_attributes = pre | post
1425
+ new_deps = [tmp]
1426
+ for dep in deps:
1427
+ left, right = dep
1428
+ if left | right <= new_attributes:
1429
+ new_deps.append(dep)
1430
+ fds = FunctionalDependencySet(new_attributes, "R" + str(i))
1431
+ if verbose:
1432
+ print(f"creating a new fdset with attributes {new_attributes}")
1433
+ generatedTasks.append(
1434
+ (
1435
+ f"Es wird eine neue Relation mit Attributmenge {new_attributes} erstellt.",
1436
+ True,
1437
+ )
1438
+ )
1439
+ i += 1
1440
+ for dep in new_deps:
1441
+ left, right = dep
1442
+ fds.add_dependency(left, right)
1443
+ if len(fds.dependencies) > 1:
1444
+ existsDepWithMultipleFDs = True
1445
+ generatedTasks.append(
1446
+ (
1447
+ f"Es gibt eine Abhängigkeit, die in mehreren Relationen enthalten ist.",
1448
+ True,
1449
+ )
1450
+ )
1451
+ new_fdsets.append(fds)
1452
+
1453
+ if existsDepWithMultipleFDs:
1454
+ generatedTasks.append(
1455
+ (
1456
+ f"Es gibt eine Abhängigkeit, die in mehreren Relationen enthalten ist.",
1457
+ True,
1458
+ )
1459
+ )
1460
+ else:
1461
+ generatedTasks.append(
1462
+ (
1463
+ f"Es gibt eine Abhängigkeit, die in mehreren Relationen enthalten ist.",
1464
+ False,
1465
+ )
1466
+ )
1467
+
1468
+ generatedTasks.append(
1469
+ (
1470
+ f"Es wird eine neue Relation pro Abhängigkeit in der kanonischen Überdeckung erstellt.",
1471
+ True,
1472
+ )
1473
+ )
1474
+ generatedTasks.append(
1475
+ (
1476
+ f"Es wird eine neue Relation pro Abhängigkeit in der ursprünglichen Menge funktionaler Abhängigkeiten erstellt.",
1477
+ False,
1478
+ )
1479
+ )
1480
+ generatedTasks.append(
1481
+ (
1482
+ f"Es wird für jeden Schlüsselkandidaten eine neue Relation erstellt.",
1483
+ False,
1484
+ )
1485
+ )
1486
+ generatedTasks.append(
1487
+ (
1488
+ f"Es werden insgesamt drei neue Relationen erstellt.",
1489
+ len(new_fdsets) == 3,
1490
+ )
1491
+ )
1492
+ generatedTasks.append(
1493
+ (
1494
+ f"Die Abhängigkeit {self.stringify_dependencies(fdsToStringify=[fds.dependencies[0]])} ist in keiner neuen Relation enthalten.",
1495
+ False,
1496
+ )
1497
+ )
1498
+
1499
+ if genEx:
1500
+ print(f"=======================================")
1501
+ print(f"Aussagen zum Hinzufügen von Relationen (Schritt 2):")
1502
+ for task in generatedTasks:
1503
+ print(task)
1504
+ print(f"=======================================")
1505
+
1506
+ return new_fdsets
1507
+
1508
+ def isCorrectCreationOfNewFDS(self, fdsets: list, verbose: bool = False):
1509
+ """
1510
+ Checks if fdsets were correctly created from canonical cover
1511
+
1512
+ Args:
1513
+ fdsets(list): List of fds
1514
+ verbose(bool): Debugging tool
1515
+ Return:
1516
+ bool: Correct?
1517
+ """
1518
+ if verbose:
1519
+ print("CHECKING: Is the proposed list of fdsets a correct solution:\n")
1520
+ print("1. Are there more new sets than dependencies:")
1521
+ if len(fdsets) > len(self.dependencies):
1522
+ if verbose:
1523
+ print(
1524
+ f"FAILURE: The number of fdsets, {len(fdsets)}, if bigger than the number of dependencies, {len(self.dependencies)}."
1525
+ )
1526
+
1527
+ if verbose:
1528
+ print("2. Have the dependencies and attributes been correctly transfered:")
1529
+ depsCopy = [0] * len(self.dependencies)
1530
+ for fds in fdsets:
1531
+ for dep in fds.dependencies:
1532
+ left, right = dep
1533
+ if not left | right <= fds.attributes:
1534
+ if verbose:
1535
+ print(
1536
+ f"FAILURE: The dependency, {left} -> {right}, has more attributes than {fds.attributes}"
1537
+ )
1538
+ return False
1539
+ if (left, right) in self.dependencies:
1540
+ i = self.dependencies.index((left, right))
1541
+ depsCopy[i] = 1
1542
+ else:
1543
+ if verbose:
1544
+ print(
1545
+ f"FAILURE: The dependency, {left} -> {right}, is not in self.dependencies."
1546
+ )
1547
+ return False
1548
+ if numpy.prod(depsCopy) != 1:
1549
+ if verbose:
1550
+ print(
1551
+ f"FAILURE: {len(depsCopy)} dependencies where not part of the solution."
1552
+ )
1553
+ return False
1554
+ if verbose:
1555
+ print("SUCCESS: The creation of fdsets was correct.")
1556
+ return True
1557
+
1558
+ def synthesize(self, verbose: bool = False, genEx: bool = False):
1559
+ """
1560
+ synthesize algorithm
1561
+
1562
+ see https://git.rwth-aachen.de/i5/teaching/dbis-vl/-/raw/main/6-RelDesign/6-RelationaleEntwurfstheorie.pdf#page=76
1563
+ and Kemper page 197
1564
+
1565
+ Args:
1566
+ verbose(bool): if True show steps
1567
+ genEx(bool): if True generate exercise tasks in form of sentences that can be either true or false
1568
+
1569
+ Return:
1570
+ list[FunctionalDependencySet]: list of synthesized fdsets deriving from this fdset
1571
+ """
1572
+ generatedTasks = []
1573
+
1574
+ keys = self.find_candidate_keys(verbose=verbose, genEx=genEx)
1575
+ self.canonical_cover(verbose, genEx)
1576
+ fdsets = self.create_new_fdsets(verbose, genEx)
1577
+ fdsets_before_key_relation = fdsets.copy()
1578
+ fdsets_with_key = self.create_optional_key_scheme(keys, fdsets, verbose, genEx)
1579
+ reduced_fdsets = self.remove_subset_relations(fdsets_with_key, verbose, genEx)
1580
+
1581
+ generatedTasks.append(
1582
+ (
1583
+ f"Wir erhalten zum Schluss die gleiche Anzahl an Relationen wie vor Schritt 3.",
1584
+ len(fdsets_before_key_relation) == len(reduced_fdsets),
1585
+ )
1586
+ )
1587
+
1588
+ if genEx:
1589
+ print(f"=======================================")
1590
+ print(f"Aussagen zum Endergebnis des Synthesealgorithmus (nach Schritt 4):")
1591
+ for task in generatedTasks:
1592
+ print(task)
1593
+ print(f"=======================================")
1594
+
1595
+ return reduced_fdsets
1596
+
1597
+ def create_optional_key_scheme(
1598
+ self, keys, fdsets, verbose: bool = False, genEx: bool = False
1599
+ ):
1600
+ """
1601
+ creates a new fdset if key is not subset of any of the existing sets attributes
1602
+
1603
+ Args:
1604
+ verbose(bool): if True show steps
1605
+ genEx(bool): if True generate exercise tasks in form of sentences that can be either true or false
1606
+
1607
+ Return:
1608
+ list[FunctionalDependencySet]: The list of fdsets with relation that has key candidate of original scheme
1609
+ """
1610
+ generatedTasks = []
1611
+
1612
+ for key in keys:
1613
+ for fds in fdsets:
1614
+ if set(key) <= fds.attributes:
1615
+ generatedTasks.append(
1616
+ (
1617
+ f"Es braucht keine Schlüsselrelation hinzugefügt werden, da die Attribute von mindestens einem der {len(key) + 1} Schlüsselkandidaten in mindestens einer neuen Relation enthalten sind.",
1618
+ False,
1619
+ )
1620
+ )
1621
+ generatedTasks.append(
1622
+ (
1623
+ f"Wir können eine neue Schlüsselrelation mit der Attributmenge {set(keys[0])} hinzufügen.",
1624
+ False,
1625
+ )
1626
+ )
1627
+ generatedTasks.append(
1628
+ (
1629
+ f"Die Relation mit Attributmenge {fds.attributes} enthält alle Attribute eines Schlüsselkandidaten.",
1630
+ True,
1631
+ )
1632
+ )
1633
+ if genEx:
1634
+ print(f"=======================================")
1635
+ print(
1636
+ f"Aussagen zum Hinzufügen einer Schlüsselrelation (Schritt 3):"
1637
+ )
1638
+ for task in generatedTasks:
1639
+ print(task)
1640
+ print(f"=======================================")
1641
+ return fdsets
1642
+ generatedTasks.append(
1643
+ (
1644
+ f"Es muss eine neue Schlüsselrelation hinzugefügt werden, da keine aktuelle Relation die Attribute eines Schlüsselkandidaten enthält.",
1645
+ True,
1646
+ )
1647
+ )
1648
+ generatedTasks.append(
1649
+ (
1650
+ f"Die hinzuzufügende Schlüsselrelation ist in der Attributmenge eindeutig.",
1651
+ not (len(keys) > 1),
1652
+ )
1653
+ )
1654
+ key = set(keys[0])
1655
+ generatedTasks.append(
1656
+ (
1657
+ f"Wir können eine neue Schlüsselrelation mit der Attributmenge {key} hinzufügen.",
1658
+ True,
1659
+ )
1660
+ )
1661
+ fds = FunctionalDependencySet(key, "R" + str(len(fdsets) + 1))
1662
+ m = len(key) // 2
1663
+ # fds.add_dependency(sorted(key)[:m], sorted(key)[m:])
1664
+ fds.add_dependency(key, key)
1665
+ fdsets.append(fds)
1666
+
1667
+ if verbose:
1668
+ print(f"adding a new fdset with attributes {key}")
1669
+ if genEx:
1670
+ print(f"=======================================")
1671
+ print(f"Aussagen zum Hinzufügen einer Schlüsselrelation (Schritt 3):")
1672
+ for task in generatedTasks:
1673
+ print(task)
1674
+ print(f"=======================================")
1675
+
1676
+ return fdsets
1677
+
1678
+ def remove_subset_relations(
1679
+ self, fdsets, verbose: bool = False, genEx: bool = False
1680
+ ):
1681
+ """
1682
+ removes fdsets with attributes that are a subset of another fdset
1683
+
1684
+ Args:
1685
+ verbose(bool): if True show steps
1686
+ genEx(bool): if True generate exercise tasks in form of sentences that can be either true or false
1687
+
1688
+ Return:
1689
+ list[FunctionalDependencySet]: The reduced list of fdsets
1690
+ """
1691
+ generatedTasks = []
1692
+ removeCounter = 0
1693
+
1694
+ if self.debug:
1695
+ print(fdsets)
1696
+ for fds in fdsets.copy():
1697
+ attributes = fds.attributes
1698
+ conflict = next(
1699
+ (
1700
+ fdset
1701
+ for fdset in fdsets
1702
+ if fds.title != fdset.title and attributes <= fdset.attributes
1703
+ ),
1704
+ None,
1705
+ )
1706
+ if conflict is not None:
1707
+ fdsets.remove(fds)
1708
+ removeCounter += 1
1709
+ generatedTasks.append(
1710
+ (
1711
+ f"Die Relation mit Attributmenge {fds.attributes} wird entfernt.",
1712
+ True,
1713
+ )
1714
+ )
1715
+ if len(fds.dependencies) > 0:
1716
+ generatedTasks.append(
1717
+ (
1718
+ f"Es muss die Relation entfernt werden, die die Abhängigkeit {fds.dependencies[0]} enthält.",
1719
+ True,
1720
+ )
1721
+ )
1722
+
1723
+ removeCounter += 1
1724
+ generatedTasks.append(
1725
+ (
1726
+ f"Die Relation mit Attributmenge {fds.attributes} wird entfernt.",
1727
+ True,
1728
+ )
1729
+ )
1730
+ if len(fds.dependencies) > 0:
1731
+ generatedTasks.append(
1732
+ (
1733
+ f"Es muss die Relation entfernt werden, die die Abhängigkeit {fds.dependencies[0]} enthält.",
1734
+ True,
1735
+ )
1736
+ )
1737
+ if verbose:
1738
+ print(f"removing the fdset {fds}")
1739
+ else:
1740
+ generatedTasks.append(
1741
+ (
1742
+ f"Die Relation mit Attributmenge {fds.attributes} wird entfernt.",
1743
+ False,
1744
+ )
1745
+ )
1746
+
1747
+ generatedTasks.append(
1748
+ (
1749
+ f"Es gibt Relationen, deren Attributmengen Teilmengen voneinander sind.",
1750
+ removeCounter > 0,
1751
+ )
1752
+ )
1753
+
1754
+ if genEx:
1755
+ print(f"=======================================")
1756
+ print(f"Aussagen zum Entfernen von Relationen (Schritt 4):")
1757
+ for task in generatedTasks:
1758
+ print(task)
1759
+ print(f"=======================================")
1760
+
1761
+ return fdsets