mirror of
https://github.com/dancojocaru2000/logic-circuits-simulator.git
synced 2025-02-21 16:49:36 +02:00
Implemented logic expressions
Also made truth table horizontally scrollable
This commit is contained in:
parent
cf71d5457a
commit
85838c2b32
10 changed files with 946 additions and 46 deletions
72
lib/components/logic_expression_field.dart
Normal file
72
lib/components/logic_expression_field.dart
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:logic_circuits_simulator/utils/logic_expressions.dart';
|
||||||
|
import 'package:logic_circuits_simulator/utils/logic_operators.dart';
|
||||||
|
|
||||||
|
class LogicExpressionField extends HookWidget {
|
||||||
|
final ValueListenable<List<String>> inputsListener;
|
||||||
|
final String outputName;
|
||||||
|
final String? initialText;
|
||||||
|
final void Function(String input, LogicExpression expression)? onChanged;
|
||||||
|
final void Function()? onInputError;
|
||||||
|
|
||||||
|
const LogicExpressionField({required this.inputsListener, required this.outputName, this.initialText, this.onChanged, this.onInputError, super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final inputs = useValueListenable(inputsListener);
|
||||||
|
final controller = useTextEditingController(text: initialText);
|
||||||
|
final errorText = useState<String?>(null);
|
||||||
|
useValueListenable(controller);
|
||||||
|
|
||||||
|
final onChg = useMemoized(() => (String newValue) {
|
||||||
|
final trimmed = newValue.trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (trimmed.isEmpty) {
|
||||||
|
onChanged?.call('', LogicExpression(operator: FalseLogicOperator(), arguments: []));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
final newLogicExpression = LogicExpression.parse(trimmed);
|
||||||
|
|
||||||
|
// Check if unknown inputs are used
|
||||||
|
final newInputs = newLogicExpression.inputs;
|
||||||
|
final unknownInputs = newInputs.where((input) => !inputs.contains(input)).toList();
|
||||||
|
if (unknownInputs.isNotEmpty) {
|
||||||
|
throw Exception('Unknown inputs found: ${unknownInputs.join(", ")}');
|
||||||
|
}
|
||||||
|
|
||||||
|
onChanged?.call(trimmed, newLogicExpression);
|
||||||
|
}
|
||||||
|
errorText.value = null;
|
||||||
|
} catch (e) {
|
||||||
|
errorText.value = e.toString();
|
||||||
|
onInputError?.call();
|
||||||
|
}
|
||||||
|
}, [inputs, errorText]);
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
if (controller.text.isNotEmpty) {
|
||||||
|
scheduleMicrotask(() {
|
||||||
|
onChg(controller.text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[inputs],
|
||||||
|
);
|
||||||
|
|
||||||
|
return TextField(
|
||||||
|
controller: controller,
|
||||||
|
onChanged: onChg,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: 'Logic Experssion for $outputName',
|
||||||
|
errorText: errorText.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,7 +24,9 @@ class ComponentEntry with _$ComponentEntry {
|
||||||
@JsonKey(includeIfNull: false)
|
@JsonKey(includeIfNull: false)
|
||||||
List<String>? truthTable,
|
List<String>? truthTable,
|
||||||
@JsonKey(includeIfNull: false)
|
@JsonKey(includeIfNull: false)
|
||||||
String? logicExpression,
|
List<String>? logicExpression,
|
||||||
|
@JsonKey(defaultValue: false)
|
||||||
|
required bool visualDesigned,
|
||||||
}) = _ComponentEntry;
|
}) = _ComponentEntry;
|
||||||
|
|
||||||
factory ComponentEntry.fromJson(Map<String, Object?> json) => _$ComponentEntryFromJson(json);
|
factory ComponentEntry.fromJson(Map<String, Object?> json) => _$ComponentEntryFromJson(json);
|
||||||
|
|
|
@ -167,7 +167,9 @@ mixin _$ComponentEntry {
|
||||||
@JsonKey(includeIfNull: false)
|
@JsonKey(includeIfNull: false)
|
||||||
List<String>? get truthTable => throw _privateConstructorUsedError;
|
List<String>? get truthTable => throw _privateConstructorUsedError;
|
||||||
@JsonKey(includeIfNull: false)
|
@JsonKey(includeIfNull: false)
|
||||||
String? get logicExpression => throw _privateConstructorUsedError;
|
List<String>? get logicExpression => throw _privateConstructorUsedError;
|
||||||
|
@JsonKey(defaultValue: false)
|
||||||
|
bool get visualDesigned => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
|
@ -187,7 +189,8 @@ abstract class $ComponentEntryCopyWith<$Res> {
|
||||||
List<String> inputs,
|
List<String> inputs,
|
||||||
List<String> outputs,
|
List<String> outputs,
|
||||||
@JsonKey(includeIfNull: false) List<String>? truthTable,
|
@JsonKey(includeIfNull: false) List<String>? truthTable,
|
||||||
@JsonKey(includeIfNull: false) String? logicExpression});
|
@JsonKey(includeIfNull: false) List<String>? logicExpression,
|
||||||
|
@JsonKey(defaultValue: false) bool visualDesigned});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
@ -208,6 +211,7 @@ class _$ComponentEntryCopyWithImpl<$Res>
|
||||||
Object? outputs = freezed,
|
Object? outputs = freezed,
|
||||||
Object? truthTable = freezed,
|
Object? truthTable = freezed,
|
||||||
Object? logicExpression = freezed,
|
Object? logicExpression = freezed,
|
||||||
|
Object? visualDesigned = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_value.copyWith(
|
return _then(_value.copyWith(
|
||||||
componentId: componentId == freezed
|
componentId: componentId == freezed
|
||||||
|
@ -237,7 +241,11 @@ class _$ComponentEntryCopyWithImpl<$Res>
|
||||||
logicExpression: logicExpression == freezed
|
logicExpression: logicExpression == freezed
|
||||||
? _value.logicExpression
|
? _value.logicExpression
|
||||||
: logicExpression // ignore: cast_nullable_to_non_nullable
|
: logicExpression // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,
|
as List<String>?,
|
||||||
|
visualDesigned: visualDesigned == freezed
|
||||||
|
? _value.visualDesigned
|
||||||
|
: visualDesigned // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -256,7 +264,8 @@ abstract class _$$_ComponentEntryCopyWith<$Res>
|
||||||
List<String> inputs,
|
List<String> inputs,
|
||||||
List<String> outputs,
|
List<String> outputs,
|
||||||
@JsonKey(includeIfNull: false) List<String>? truthTable,
|
@JsonKey(includeIfNull: false) List<String>? truthTable,
|
||||||
@JsonKey(includeIfNull: false) String? logicExpression});
|
@JsonKey(includeIfNull: false) List<String>? logicExpression,
|
||||||
|
@JsonKey(defaultValue: false) bool visualDesigned});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
@ -279,6 +288,7 @@ class __$$_ComponentEntryCopyWithImpl<$Res>
|
||||||
Object? outputs = freezed,
|
Object? outputs = freezed,
|
||||||
Object? truthTable = freezed,
|
Object? truthTable = freezed,
|
||||||
Object? logicExpression = freezed,
|
Object? logicExpression = freezed,
|
||||||
|
Object? visualDesigned = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_$_ComponentEntry(
|
return _then(_$_ComponentEntry(
|
||||||
componentId: componentId == freezed
|
componentId: componentId == freezed
|
||||||
|
@ -306,9 +316,13 @@ class __$$_ComponentEntryCopyWithImpl<$Res>
|
||||||
: truthTable // ignore: cast_nullable_to_non_nullable
|
: truthTable // ignore: cast_nullable_to_non_nullable
|
||||||
as List<String>?,
|
as List<String>?,
|
||||||
logicExpression: logicExpression == freezed
|
logicExpression: logicExpression == freezed
|
||||||
? _value.logicExpression
|
? _value._logicExpression
|
||||||
: logicExpression // ignore: cast_nullable_to_non_nullable
|
: logicExpression // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,
|
as List<String>?,
|
||||||
|
visualDesigned: visualDesigned == freezed
|
||||||
|
? _value.visualDesigned
|
||||||
|
: visualDesigned // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -323,10 +337,12 @@ class _$_ComponentEntry implements _ComponentEntry {
|
||||||
required final List<String> inputs,
|
required final List<String> inputs,
|
||||||
required final List<String> outputs,
|
required final List<String> outputs,
|
||||||
@JsonKey(includeIfNull: false) final List<String>? truthTable,
|
@JsonKey(includeIfNull: false) final List<String>? truthTable,
|
||||||
@JsonKey(includeIfNull: false) this.logicExpression})
|
@JsonKey(includeIfNull: false) final List<String>? logicExpression,
|
||||||
|
@JsonKey(defaultValue: false) required this.visualDesigned})
|
||||||
: _inputs = inputs,
|
: _inputs = inputs,
|
||||||
_outputs = outputs,
|
_outputs = outputs,
|
||||||
_truthTable = truthTable;
|
_truthTable = truthTable,
|
||||||
|
_logicExpression = logicExpression;
|
||||||
|
|
||||||
factory _$_ComponentEntry.fromJson(Map<String, dynamic> json) =>
|
factory _$_ComponentEntry.fromJson(Map<String, dynamic> json) =>
|
||||||
_$$_ComponentEntryFromJson(json);
|
_$$_ComponentEntryFromJson(json);
|
||||||
|
@ -362,13 +378,23 @@ class _$_ComponentEntry implements _ComponentEntry {
|
||||||
return EqualUnmodifiableListView(value);
|
return EqualUnmodifiableListView(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final List<String>? _logicExpression;
|
||||||
@override
|
@override
|
||||||
@JsonKey(includeIfNull: false)
|
@JsonKey(includeIfNull: false)
|
||||||
final String? logicExpression;
|
List<String>? get logicExpression {
|
||||||
|
final value = _logicExpression;
|
||||||
|
if (value == null) return null;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@JsonKey(defaultValue: false)
|
||||||
|
final bool visualDesigned;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ComponentEntry(componentId: $componentId, componentName: $componentName, componentDescription: $componentDescription, inputs: $inputs, outputs: $outputs, truthTable: $truthTable, logicExpression: $logicExpression)';
|
return 'ComponentEntry(componentId: $componentId, componentName: $componentName, componentDescription: $componentDescription, inputs: $inputs, outputs: $outputs, truthTable: $truthTable, logicExpression: $logicExpression, visualDesigned: $visualDesigned)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -387,7 +413,9 @@ class _$_ComponentEntry implements _ComponentEntry {
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other._truthTable, _truthTable) &&
|
.equals(other._truthTable, _truthTable) &&
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other.logicExpression, logicExpression));
|
.equals(other._logicExpression, _logicExpression) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other.visualDesigned, visualDesigned));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
|
@ -400,7 +428,8 @@ class _$_ComponentEntry implements _ComponentEntry {
|
||||||
const DeepCollectionEquality().hash(_inputs),
|
const DeepCollectionEquality().hash(_inputs),
|
||||||
const DeepCollectionEquality().hash(_outputs),
|
const DeepCollectionEquality().hash(_outputs),
|
||||||
const DeepCollectionEquality().hash(_truthTable),
|
const DeepCollectionEquality().hash(_truthTable),
|
||||||
const DeepCollectionEquality().hash(logicExpression));
|
const DeepCollectionEquality().hash(_logicExpression),
|
||||||
|
const DeepCollectionEquality().hash(visualDesigned));
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@override
|
@override
|
||||||
|
@ -421,7 +450,8 @@ abstract class _ComponentEntry implements ComponentEntry {
|
||||||
required final List<String> inputs,
|
required final List<String> inputs,
|
||||||
required final List<String> outputs,
|
required final List<String> outputs,
|
||||||
@JsonKey(includeIfNull: false) final List<String>? truthTable,
|
@JsonKey(includeIfNull: false) final List<String>? truthTable,
|
||||||
@JsonKey(includeIfNull: false) final String? logicExpression}) =
|
@JsonKey(includeIfNull: false) final List<String>? logicExpression,
|
||||||
|
@JsonKey(defaultValue: false) required final bool visualDesigned}) =
|
||||||
_$_ComponentEntry;
|
_$_ComponentEntry;
|
||||||
|
|
||||||
factory _ComponentEntry.fromJson(Map<String, dynamic> json) =
|
factory _ComponentEntry.fromJson(Map<String, dynamic> json) =
|
||||||
|
@ -443,7 +473,10 @@ abstract class _ComponentEntry implements ComponentEntry {
|
||||||
List<String>? get truthTable => throw _privateConstructorUsedError;
|
List<String>? get truthTable => throw _privateConstructorUsedError;
|
||||||
@override
|
@override
|
||||||
@JsonKey(includeIfNull: false)
|
@JsonKey(includeIfNull: false)
|
||||||
String? get logicExpression => throw _privateConstructorUsedError;
|
List<String>? get logicExpression => throw _privateConstructorUsedError;
|
||||||
|
@override
|
||||||
|
@JsonKey(defaultValue: false)
|
||||||
|
bool get visualDesigned => throw _privateConstructorUsedError;
|
||||||
@override
|
@override
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
_$$_ComponentEntryCopyWith<_$_ComponentEntry> get copyWith =>
|
_$$_ComponentEntryCopyWith<_$_ComponentEntry> get copyWith =>
|
||||||
|
|
|
@ -30,7 +30,10 @@ _$_ComponentEntry _$$_ComponentEntryFromJson(Map<String, dynamic> json) =>
|
||||||
truthTable: (json['truthTable'] as List<dynamic>?)
|
truthTable: (json['truthTable'] as List<dynamic>?)
|
||||||
?.map((e) => e as String)
|
?.map((e) => e as String)
|
||||||
.toList(),
|
.toList(),
|
||||||
logicExpression: json['logicExpression'] as String?,
|
logicExpression: (json['logicExpression'] as List<dynamic>?)
|
||||||
|
?.map((e) => e as String)
|
||||||
|
.toList(),
|
||||||
|
visualDesigned: json['visualDesigned'] as bool? ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$_ComponentEntryToJson(_$_ComponentEntry instance) {
|
Map<String, dynamic> _$$_ComponentEntryToJson(_$_ComponentEntry instance) {
|
||||||
|
@ -50,5 +53,6 @@ Map<String, dynamic> _$$_ComponentEntryToJson(_$_ComponentEntry instance) {
|
||||||
val['outputs'] = instance.outputs;
|
val['outputs'] = instance.outputs;
|
||||||
writeNotNull('truthTable', instance.truthTable);
|
writeNotNull('truthTable', instance.truthTable);
|
||||||
writeNotNull('logicExpression', instance.logicExpression);
|
writeNotNull('logicExpression', instance.logicExpression);
|
||||||
|
val['visualDesigned'] = instance.visualDesigned;
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,14 @@ import 'dart:math';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:logic_circuits_simulator/components/logic_expression_field.dart';
|
||||||
import 'package:logic_circuits_simulator/components/truth_table.dart';
|
import 'package:logic_circuits_simulator/components/truth_table.dart';
|
||||||
import 'package:logic_circuits_simulator/dialogs/new_ask_for_name.dart';
|
import 'package:logic_circuits_simulator/dialogs/new_ask_for_name.dart';
|
||||||
import 'package:logic_circuits_simulator/models/project.dart';
|
import 'package:logic_circuits_simulator/models/project.dart';
|
||||||
import 'package:logic_circuits_simulator/state/project.dart';
|
import 'package:logic_circuits_simulator/state/project.dart';
|
||||||
import 'package:logic_circuits_simulator/utils/iterable_extension.dart';
|
import 'package:logic_circuits_simulator/utils/iterable_extension.dart';
|
||||||
|
import 'package:logic_circuits_simulator/utils/logic_expressions.dart';
|
||||||
|
import 'package:logic_circuits_simulator/utils/logic_operators.dart';
|
||||||
import 'package:logic_circuits_simulator/utils/provider_hook.dart';
|
import 'package:logic_circuits_simulator/utils/provider_hook.dart';
|
||||||
|
|
||||||
class EditComponentPage extends HookWidget {
|
class EditComponentPage extends HookWidget {
|
||||||
|
@ -24,13 +27,21 @@ class EditComponentPage extends HookWidget {
|
||||||
final projectState = useProvider<ProjectState>();
|
final projectState = useProvider<ProjectState>();
|
||||||
ComponentEntry ce() => projectState.index.components.where((c) => c.componentId == component.componentId).first;
|
ComponentEntry ce() => projectState.index.components.where((c) => c.componentId == component.componentId).first;
|
||||||
final truthTable = useState(ce().truthTable?.toList());
|
final truthTable = useState(ce().truthTable?.toList());
|
||||||
final logicExpression = useState(ce().logicExpression);
|
final logicExpressions = useState(ce().logicExpression);
|
||||||
|
final logicExpressionsParsed = useState(
|
||||||
|
logicExpressions.value == null
|
||||||
|
? null
|
||||||
|
: List<LogicExpression?>.generate(logicExpressions.value!.length, (index) => null),
|
||||||
|
);
|
||||||
|
final visualDesigned = useState(ce().visualDesigned);
|
||||||
final inputs = useState(ce().inputs.toList());
|
final inputs = useState(ce().inputs.toList());
|
||||||
final outputs = useState(ce().outputs.toList());
|
final outputs = useState(ce().outputs.toList());
|
||||||
final componentNameEditingController = useTextEditingController(text: ce().componentName);
|
final componentNameEditingController = useTextEditingController(text: ce().componentName);
|
||||||
useValueListenable(componentNameEditingController);
|
useValueListenable(componentNameEditingController);
|
||||||
final dirty = useMemoized(
|
final dirty = useMemoized(
|
||||||
() {
|
() {
|
||||||
|
const le = ListEquality();
|
||||||
|
|
||||||
if (componentNameEditingController.text.isEmpty) {
|
if (componentNameEditingController.text.isEmpty) {
|
||||||
// Don't allow saving empty name
|
// Don't allow saving empty name
|
||||||
return false;
|
return false;
|
||||||
|
@ -43,24 +54,31 @@ class EditComponentPage extends HookWidget {
|
||||||
// Don't allow saving empty outputs
|
// Don't allow saving empty outputs
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (truthTable.value == null && logicExpression.value == null) {
|
if (truthTable.value == null && logicExpressions.value == null && !visualDesigned.value) {
|
||||||
// Don't allow saving components without functionality
|
// Don't allow saving components without functionality
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (logicExpressionsParsed.value != null && logicExpressionsParsed.value?.contains(null) != false) {
|
||||||
|
// Don't allow saving components with errors in parsing logic expressions
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (componentNameEditingController.text != ce().componentName) {
|
if (componentNameEditingController.text != ce().componentName) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!const ListEquality().equals(inputs.value, ce().inputs)) {
|
if (!le.equals(inputs.value, ce().inputs)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!const ListEquality().equals(outputs.value, ce().outputs)) {
|
if (!le.equals(outputs.value, ce().outputs)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!const ListEquality().equals(truthTable.value, ce().truthTable)) {
|
if (!le.equals(truthTable.value, ce().truthTable)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (logicExpression.value != ce().logicExpression) {
|
if (!le.equals(logicExpressions.value, ce().logicExpression)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (visualDesigned.value != ce().visualDesigned) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -74,9 +92,31 @@ class EditComponentPage extends HookWidget {
|
||||||
ce().outputs,
|
ce().outputs,
|
||||||
truthTable.value,
|
truthTable.value,
|
||||||
ce().truthTable,
|
ce().truthTable,
|
||||||
|
logicExpressions.value,
|
||||||
|
ce().logicExpression,
|
||||||
|
visualDesigned.value,
|
||||||
|
ce().visualDesigned,
|
||||||
|
logicExpressionsParsed.value,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final updateTTFromLE = useMemoized(
|
||||||
|
() {
|
||||||
|
return () {
|
||||||
|
if (logicExpressionsParsed.value?.contains(null) != false) {
|
||||||
|
truthTable.value = null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
truthTable.value = logicExpressionsParsed.value!.first!.computeTruthTable(inputs.value).zipWith(
|
||||||
|
logicExpressionsParsed.value!.skip(1).map((le) => le!.computeTruthTable(inputs.value)),
|
||||||
|
(args) => args.join(),
|
||||||
|
).toList();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[logicExpressions.value, truthTable.value]
|
||||||
|
);
|
||||||
|
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
onWillPop: () async {
|
onWillPop: () async {
|
||||||
if (!dirty && !newComponent) {
|
if (!dirty && !newComponent) {
|
||||||
|
@ -137,7 +177,7 @@ class EditComponentPage extends HookWidget {
|
||||||
),
|
),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
@ -161,7 +201,13 @@ class EditComponentPage extends HookWidget {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (inputName != null) {
|
if (inputName != null) {
|
||||||
truthTable.value = truthTable.value?.expand((element) => [element, element]).toList();
|
if (logicExpressions.value != null) {
|
||||||
|
// They should update themselves
|
||||||
|
updateTTFromLE();
|
||||||
|
}
|
||||||
|
else if (truthTable.value != null) {
|
||||||
|
truthTable.value = truthTable.value?.expand((element) => [element, element]).toList();
|
||||||
|
}
|
||||||
inputs.value = inputs.value.toList()..add(inputName);
|
inputs.value = inputs.value.toList()..add(inputName);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -205,7 +251,11 @@ class EditComponentPage extends HookWidget {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (shouldRemove == true) {
|
if (shouldRemove == true) {
|
||||||
if (truthTable.value != null) {
|
if (logicExpressions.value != null) {
|
||||||
|
// They should update themselves
|
||||||
|
updateTTFromLE();
|
||||||
|
}
|
||||||
|
else if (truthTable.value != null) {
|
||||||
final tt = truthTable.value!.toList();
|
final tt = truthTable.value!.toList();
|
||||||
final shiftIndex = inputs.value.length - 1 - idx;
|
final shiftIndex = inputs.value.length - 1 - idx;
|
||||||
final shifted = 1 << shiftIndex;
|
final shifted = 1 << shiftIndex;
|
||||||
|
@ -230,7 +280,7 @@ class EditComponentPage extends HookWidget {
|
||||||
),
|
),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
@ -254,7 +304,14 @@ class EditComponentPage extends HookWidget {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (outputName != null) {
|
if (outputName != null) {
|
||||||
truthTable.value = truthTable.value?.map((e) => '${e}0').toList();
|
if (logicExpressions.value != null) {
|
||||||
|
logicExpressions.value = logicExpressions.value!.followedBy(['']).toList();
|
||||||
|
logicExpressionsParsed.value = logicExpressionsParsed.value?.followedBy([LogicExpression.ofZeroOp(FalseLogicOperator())]).toList();
|
||||||
|
updateTTFromLE();
|
||||||
|
}
|
||||||
|
else if (truthTable.value != null) {
|
||||||
|
truthTable.value = truthTable.value?.map((e) => '${e}0').toList();
|
||||||
|
}
|
||||||
outputs.value = outputs.value.toList()..add(outputName);
|
outputs.value = outputs.value.toList()..add(outputName);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -298,7 +355,12 @@ class EditComponentPage extends HookWidget {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (shouldRemove == true) {
|
if (shouldRemove == true) {
|
||||||
if (truthTable.value != null) {
|
if (logicExpressions.value != null) {
|
||||||
|
logicExpressions.value = logicExpressions.value?.toList()?..replaceRange(idx, idx+1, []);
|
||||||
|
logicExpressionsParsed.value = logicExpressionsParsed.value?.toList()?..replaceRange(idx, idx+1, []);
|
||||||
|
updateTTFromLE();
|
||||||
|
}
|
||||||
|
else if (truthTable.value != null) {
|
||||||
for (var i = 0; i < truthTable.value!.length; i++) {
|
for (var i = 0; i < truthTable.value!.length; i++) {
|
||||||
truthTable.value!.replaceRange(i, i+1, [truthTable.value![i].replaceRange(idx, idx+1, "")]);
|
truthTable.value!.replaceRange(i, i+1, [truthTable.value![i].replaceRange(idx, idx+1, "")]);
|
||||||
}
|
}
|
||||||
|
@ -311,11 +373,37 @@ class EditComponentPage extends HookWidget {
|
||||||
childCount: outputs.value.length,
|
childCount: outputs.value.length,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (truthTable.value == null && logicExpression.value == null) ...[
|
const SliverToBoxAdapter(
|
||||||
|
child: Divider(),
|
||||||
|
),
|
||||||
|
if (inputs.value.isEmpty && outputs.value.isEmpty)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Text(
|
||||||
|
'Add inputs and outputs to continue',
|
||||||
|
style: Theme.of(context).textTheme.headline4,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (inputs.value.isEmpty)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Text(
|
||||||
|
'Add inputs to continue',
|
||||||
|
style: Theme.of(context).textTheme.headline4,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (outputs.value.isEmpty)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Text(
|
||||||
|
'Add outputs to continue',
|
||||||
|
style: Theme.of(context).textTheme.headline4,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (truthTable.value == null && logicExpressions.value == null && !visualDesigned.value) ...[
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const Divider(),
|
|
||||||
Text(
|
Text(
|
||||||
'Choose component kind',
|
'Choose component kind',
|
||||||
style: Theme.of(context).textTheme.headline4,
|
style: Theme.of(context).textTheme.headline4,
|
||||||
|
@ -324,7 +412,15 @@ class EditComponentPage extends HookWidget {
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
logicExpression.value = '';
|
// For each output, a separate logic expression is needed
|
||||||
|
logicExpressions.value = List.generate(
|
||||||
|
outputs.value.length,
|
||||||
|
(index) => '',
|
||||||
|
);
|
||||||
|
logicExpressionsParsed.value = List.generate(
|
||||||
|
outputs.value.length,
|
||||||
|
(index) => null,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: const Text('Logic Expression'),
|
child: const Text('Logic Expression'),
|
||||||
),
|
),
|
||||||
|
@ -333,8 +429,13 @@ class EditComponentPage extends HookWidget {
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
// Assign false by default to each output
|
||||||
final row = "0" * outputs.value.length;
|
final row = "0" * outputs.value.length;
|
||||||
truthTable.value = List.generate(pow(2, inputs.value.length) as int, (_) => row);
|
// There are 2^inputs combinations in a truth table
|
||||||
|
truthTable.value = List.generate(
|
||||||
|
pow(2, inputs.value.length) as int,
|
||||||
|
(_) => row,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: const Text('Truth Table'),
|
child: const Text('Truth Table'),
|
||||||
),
|
),
|
||||||
|
@ -342,7 +443,9 @@ class EditComponentPage extends HookWidget {
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
onPressed: null,
|
onPressed: () {
|
||||||
|
visualDesigned.value = true;
|
||||||
|
},
|
||||||
child: const Text('Visual Designer'),
|
child: const Text('Visual Designer'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -350,33 +453,115 @@ class EditComponentPage extends HookWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (logicExpression.value != null) ...[
|
if (logicExpressions.value != null) ...[
|
||||||
|
|
||||||
],
|
|
||||||
if (truthTable.value != null) ...[
|
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Truth Table',
|
outputs.value.length == 1 ? 'Logic Expression' : 'Logic Expressions',
|
||||||
style: Theme.of(context).textTheme.headline5,
|
style: Theme.of(context).textTheme.headline5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: LogicExpressionField(
|
||||||
|
inputsListener: inputs,
|
||||||
|
outputName: outputs.value[index],
|
||||||
|
initialText: logicExpressions.value![index],
|
||||||
|
onChanged: (newValue, newExpression) {
|
||||||
|
logicExpressions.value = logicExpressions.value!.toList();
|
||||||
|
logicExpressions.value![index] = newValue;
|
||||||
|
logicExpressionsParsed.value = logicExpressionsParsed.value!.toList();
|
||||||
|
logicExpressionsParsed.value![index] = newExpression;
|
||||||
|
updateTTFromLE();
|
||||||
|
},
|
||||||
|
onInputError: () {
|
||||||
|
logicExpressionsParsed.value = logicExpressionsParsed.value!.toList();
|
||||||
|
logicExpressionsParsed.value![index] = null;
|
||||||
|
updateTTFromLE();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: logicExpressions.value!.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (truthTable.value != null) ...[
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
|
child: Text(
|
||||||
|
logicExpressions.value == null ? 'Truth Table' : 'Resulting Truth Table',
|
||||||
|
style: Theme.of(context).textTheme.headline5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (logicExpressions.value == null)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
'Tap output cells to toggle',
|
||||||
|
style: Theme.of(context).textTheme.caption,
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: TruthTableEditor(
|
child: LayoutBuilder(
|
||||||
truthTable: truthTable.value!,
|
builder: (context, constraints) {
|
||||||
inputs: inputs.value,
|
return SingleChildScrollView(
|
||||||
outputs: outputs.value,
|
scrollDirection: Axis.horizontal,
|
||||||
onUpdateTable: (idx, newValue) {
|
child: ConstrainedBox(
|
||||||
truthTable.value = truthTable.value?.toList()?..replaceRange(idx, idx+1, [newValue]);
|
constraints: BoxConstraints(minWidth: constraints.maxWidth),
|
||||||
},
|
child: TruthTableEditor(
|
||||||
|
truthTable: truthTable.value!,
|
||||||
|
inputs: inputs.value,
|
||||||
|
outputs: outputs.value,
|
||||||
|
// Only allow updating truth table if it is *NOT* autogenerated by logic expressions
|
||||||
|
onUpdateTable: logicExpressions.value != null ? null : (idx, newValue) {
|
||||||
|
truthTable.value = truthTable.value?.toList()?..replaceRange(idx, idx+1, [newValue]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
if (visualDesigned.value) ...[
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Visually Designed Component",
|
||||||
|
style: Theme.of(context).textTheme.headline4,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
if (dirty) Text(
|
||||||
|
"Save the component to open the designer",
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
) else Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: ElevatedButton(
|
||||||
|
// TODO: Implement visual designer
|
||||||
|
onPressed: null,
|
||||||
|
child: Text('Open Designer'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
const SliverPadding(
|
const SliverPadding(
|
||||||
padding: EdgeInsets.only(bottom: 56 + 16 + 16),
|
padding: EdgeInsets.only(bottom: 56 + 16 + 16),
|
||||||
),
|
),
|
||||||
|
@ -391,6 +576,8 @@ class EditComponentPage extends HookWidget {
|
||||||
inputs: inputs.value,
|
inputs: inputs.value,
|
||||||
outputs: outputs.value,
|
outputs: outputs.value,
|
||||||
truthTable: truthTable.value,
|
truthTable: truthTable.value,
|
||||||
|
logicExpression: logicExpressions.value,
|
||||||
|
visualDesigned: visualDesigned.value,
|
||||||
));
|
));
|
||||||
anySave.value = true;
|
anySave.value = true;
|
||||||
// TODO: Implement saving
|
// TODO: Implement saving
|
||||||
|
|
|
@ -76,6 +76,7 @@ class ProjectState extends ChangeNotifier {
|
||||||
componentName: '',
|
componentName: '',
|
||||||
inputs: [],
|
inputs: [],
|
||||||
outputs: [],
|
outputs: [],
|
||||||
|
visualDesigned: false,
|
||||||
);
|
);
|
||||||
await _updateIndex(index.copyWith(components: index.components + [newComponent]));
|
await _updateIndex(index.copyWith(components: index.components + [newComponent]));
|
||||||
return newComponent;
|
return newComponent;
|
||||||
|
|
|
@ -3,4 +3,22 @@ extension IndexedMap<T> on Iterable<T> {
|
||||||
int index = 0;
|
int index = 0;
|
||||||
return map((e) => toElement(index++, e));
|
return map((e) => toElement(index++, e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Iterable<O> zipWith<O>(Iterable<Iterable<T>> otherIterables, O Function(List<T>) toElement, {bool allowUnequalLengths = false}) sync* {
|
||||||
|
final iterators = [this].followedBy(otherIterables).map((e) => e.iterator).toList();
|
||||||
|
while (true) {
|
||||||
|
int ends = iterators.map((it) => it.moveNext()).where((e) => e == false).length;
|
||||||
|
if (ends != 0) {
|
||||||
|
// At least one iterator finished
|
||||||
|
if (ends != iterators.length && !allowUnequalLengths) {
|
||||||
|
throw Exception('While zipping, $ends iterators ended early');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yield toElement(iterators.map((it) => it.current).toList(growable: false));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
286
lib/utils/logic_expressions.dart
Normal file
286
lib/utils/logic_expressions.dart
Normal file
|
@ -0,0 +1,286 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:logic_circuits_simulator/utils/iterable_extension.dart';
|
||||||
|
import 'package:logic_circuits_simulator/utils/logic_operators.dart';
|
||||||
|
|
||||||
|
part 'logic_expressions.freezed.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class LogicExpression with _$LogicExpression {
|
||||||
|
const LogicExpression._();
|
||||||
|
|
||||||
|
const factory LogicExpression({
|
||||||
|
required LogicOperator operator,
|
||||||
|
required List<dynamic> arguments,
|
||||||
|
}) = _LogicExpression;
|
||||||
|
|
||||||
|
factory LogicExpression.ofZeroOp(ZeroOpLogicOperator operator) => LogicExpression(operator: operator, arguments: []);
|
||||||
|
|
||||||
|
static dynamic _classify(String token) {
|
||||||
|
final operators = [
|
||||||
|
FalseLogicOperator(),
|
||||||
|
TrueLogicOperator(),
|
||||||
|
NotLogicOperator(),
|
||||||
|
AndLogicOperator(),
|
||||||
|
OrLogicOperator(),
|
||||||
|
XorLogicOperator(),
|
||||||
|
NandLogicOperator(),
|
||||||
|
NorLogicOperator(),
|
||||||
|
XnorLogicOperator(),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final op in operators) {
|
||||||
|
if (op.representations.contains(token)) {
|
||||||
|
return op;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final inputStart = RegExp('^[A-Za-z]');
|
||||||
|
if (inputStart.hasMatch(token)) {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception('Unknown operator: $token');
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<dynamic> _tokenize(String input) {
|
||||||
|
final space = ' '.codeUnits[0];
|
||||||
|
final openedParen = '('.codeUnits[0];
|
||||||
|
final closedParen = ')'.codeUnits[0];
|
||||||
|
final transitionToOperator = RegExp('[^A-Za-z0-9]');
|
||||||
|
final transitionToInput = RegExp('[A-Za-z]');
|
||||||
|
|
||||||
|
List<dynamic> result = [];
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
bool operator = false;
|
||||||
|
int parenDepth = 0;
|
||||||
|
|
||||||
|
for (final rune in input.runes) {
|
||||||
|
if (rune == openedParen) {
|
||||||
|
if (parenDepth == 0 && buffer.isNotEmpty) {
|
||||||
|
result.add(_classify(buffer.toString()));
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
else if (parenDepth > 0) {
|
||||||
|
buffer.writeCharCode(rune);
|
||||||
|
}
|
||||||
|
parenDepth++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (rune == closedParen) {
|
||||||
|
parenDepth--;
|
||||||
|
if (parenDepth == 0) {
|
||||||
|
result.add(_tokenize(buffer.toString()));
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
else if (parenDepth < 0) {
|
||||||
|
throw Exception('Unmached parenthesis: too many closed parenthesis');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
buffer.writeCharCode(rune);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (parenDepth > 0) {
|
||||||
|
// While inside paren, just add stuff to the buffer to be further
|
||||||
|
// processed recursively and put inside of a list.
|
||||||
|
// ~(~(A&(A+B))+B& ~A)
|
||||||
|
// │ │ └───┘│ │
|
||||||
|
// │ └───────┘ │
|
||||||
|
// └────────────────┘
|
||||||
|
buffer.writeCharCode(rune);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (rune == space) {
|
||||||
|
if (buffer.isNotEmpty) {
|
||||||
|
result.add(_classify(buffer.toString()));
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (buffer.isNotEmpty) {
|
||||||
|
// Check if switching from operator to input.
|
||||||
|
// This allows an expression such as A&B to be valid.
|
||||||
|
// Switching happens when in the middle of a token
|
||||||
|
// and changing from [A-Za-z0-9] to [^A-Za-z0-9]
|
||||||
|
// or from [^A-Za-z] to [A-Za-z].
|
||||||
|
// Inputs can't start with digits.
|
||||||
|
if (!operator && transitionToOperator.hasMatch(String.fromCharCode(rune))) {
|
||||||
|
result.add(_classify(buffer.toString()));
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
else if (operator && transitionToInput.hasMatch(String.fromCharCode(rune))) {
|
||||||
|
result.add(_classify(buffer.toString()));
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (buffer.isEmpty) {
|
||||||
|
operator = !transitionToInput.hasMatch(String.fromCharCode(rune));
|
||||||
|
}
|
||||||
|
buffer.writeCharCode(rune);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parenDepth != 0) {
|
||||||
|
throw Exception('Unmached parenthesis: too many open parenthesis');
|
||||||
|
}
|
||||||
|
if (buffer.isNotEmpty) {
|
||||||
|
result.add(_classify(buffer.toString()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory LogicExpression.parse(String input) {
|
||||||
|
final tokens = _tokenize(input);
|
||||||
|
|
||||||
|
final result = LogicExpression._parse(tokens);
|
||||||
|
if (result is String) {
|
||||||
|
return LogicExpression(
|
||||||
|
operator: OrLogicOperator(),
|
||||||
|
arguments: [
|
||||||
|
result,
|
||||||
|
LogicExpression.ofZeroOp(FalseLogicOperator()),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static dynamic _parse(dynamic input) {
|
||||||
|
if (input is List) {
|
||||||
|
final tokens = input;
|
||||||
|
|
||||||
|
final orderedOpGroups = [
|
||||||
|
[OrLogicOperator(), NorLogicOperator()],
|
||||||
|
[XorLogicOperator(), XnorLogicOperator()],
|
||||||
|
[AndLogicOperator(), NandLogicOperator()],
|
||||||
|
[NotLogicOperator()],
|
||||||
|
[FalseLogicOperator(), TrueLogicOperator()],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final ops in orderedOpGroups) {
|
||||||
|
for (var i = tokens.length - 1; i >= 0; i--) {
|
||||||
|
if (ops.contains(tokens[i])) {
|
||||||
|
if (tokens[i] is ZeroOpLogicOperator) {
|
||||||
|
// ZeroOp operator should be alone
|
||||||
|
if (tokens.length != 1) {
|
||||||
|
throw Exception('ZeroOp operator should be alone');
|
||||||
|
}
|
||||||
|
return LogicExpression.ofZeroOp(tokens[i]);
|
||||||
|
}
|
||||||
|
else if (tokens[i] is OneOpLogicOperator) {
|
||||||
|
// OneOp operator should appear prefix only
|
||||||
|
// So index should be 0
|
||||||
|
if (i != 0) {
|
||||||
|
throw Exception('OneOp operator should be prefix');
|
||||||
|
}
|
||||||
|
// It should only be possible to get here if there is only one argument
|
||||||
|
// follows. The only other case is someone writing:
|
||||||
|
// ~ A B
|
||||||
|
// which would result in [NotLogicOperator, 'A', 'B'].
|
||||||
|
// Such syntax is ambiguous and should not be allowed:
|
||||||
|
// (~ A) & B -or- ~ (A & B) ?
|
||||||
|
// This is the disadvantage of linear, left-to-right notation (as opposed
|
||||||
|
// to the notation with a bar above the NOT-ed expression).
|
||||||
|
if (tokens.length > 2) {
|
||||||
|
throw Exception('Ambiguous expression: ${tokens[i]} followed by multiple tokens (${tokens.skip(1).toList()})');
|
||||||
|
}
|
||||||
|
else if (tokens.length == 1) {
|
||||||
|
throw Exception('Unfinished expression');
|
||||||
|
}
|
||||||
|
return LogicExpression(
|
||||||
|
operator: tokens[0],
|
||||||
|
arguments: [_parse(tokens[1])],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (tokens[i] is TwoOpLogicOperator) {
|
||||||
|
return LogicExpression(
|
||||||
|
operator: tokens[i],
|
||||||
|
arguments: [
|
||||||
|
_parse(tokens.getRange(0, i).toList()),
|
||||||
|
_parse(tokens.getRange(i + 1, tokens.length).toList()),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw Exception('Matched with operator that somehow isn\'t Zero/One/TwoOp');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No operators were found. This means the only tokens are props.
|
||||||
|
// If there is only one prop, return it alone:
|
||||||
|
// A => [A] => A
|
||||||
|
// If there are multiple props, apply AND:
|
||||||
|
// A B C => [A, B, C] => A & B & C => (A & B) & C
|
||||||
|
// Keep in mind the second case is only possible if the props are separated by spaces,
|
||||||
|
// as the nature of prop names allowing multiple characters only allows multiple props
|
||||||
|
// to appear one after the other if separated by spaces.
|
||||||
|
if (tokens.length == 1) {
|
||||||
|
return tokens[0];
|
||||||
|
}
|
||||||
|
else if (tokens.isEmpty) {
|
||||||
|
// This happens in unfinished expressions:
|
||||||
|
// A ^ ! => XOR(A, NOT(?))
|
||||||
|
// A ^ => XOR(A, ?)
|
||||||
|
throw Exception('Unfinished expression');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
final and = AndLogicOperator();
|
||||||
|
return _parse(tokens.expand((token) => [and, token]).skip(1).toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (input is String) {
|
||||||
|
// Prop, just return.
|
||||||
|
// Happens in such cases:
|
||||||
|
// B & ~ A => & [B, ~ [A]]
|
||||||
|
// ^ ^
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> get inputs {
|
||||||
|
Set<String> result = {};
|
||||||
|
for (final arg in arguments) {
|
||||||
|
if (arg is String) {
|
||||||
|
result.add(arg);
|
||||||
|
}
|
||||||
|
else if (arg is LogicExpression) {
|
||||||
|
result.addAll(arg.inputs);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw Exception('Unknown argument type found: ${arg.runtimeType}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool evaluate(Map<String, bool> inputs) {
|
||||||
|
return operator.apply(
|
||||||
|
arguments
|
||||||
|
.map(
|
||||||
|
// If the argument is a logical expression, evaluate recursively
|
||||||
|
// else it must be an input name, so replace based on supplied mapping.
|
||||||
|
(e) => e is LogicExpression ? e.evaluate(inputs) : inputs[e]!
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> computeTruthTable(List<String> inputs) {
|
||||||
|
final ttRows = pow(2, inputs.length) as int;
|
||||||
|
return List.generate(
|
||||||
|
ttRows,
|
||||||
|
(index) => evaluate(
|
||||||
|
{
|
||||||
|
for (var element in inputs.reversed.indexedMap((index, input) => [index, input]))
|
||||||
|
element[1] as String : (index & (pow(2, element[0] as int) as int)) != 0
|
||||||
|
},
|
||||||
|
) ? '1' : '0',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
159
lib/utils/logic_expressions.freezed.dart
Normal file
159
lib/utils/logic_expressions.freezed.dart
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
|
||||||
|
|
||||||
|
part of 'logic_expressions.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$LogicExpression {
|
||||||
|
LogicOperator get operator => throw _privateConstructorUsedError;
|
||||||
|
List<dynamic> get arguments => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
$LogicExpressionCopyWith<LogicExpression> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $LogicExpressionCopyWith<$Res> {
|
||||||
|
factory $LogicExpressionCopyWith(
|
||||||
|
LogicExpression value, $Res Function(LogicExpression) then) =
|
||||||
|
_$LogicExpressionCopyWithImpl<$Res>;
|
||||||
|
$Res call({LogicOperator operator, List<dynamic> arguments});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$LogicExpressionCopyWithImpl<$Res>
|
||||||
|
implements $LogicExpressionCopyWith<$Res> {
|
||||||
|
_$LogicExpressionCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
final LogicExpression _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function(LogicExpression) _then;
|
||||||
|
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? operator = freezed,
|
||||||
|
Object? arguments = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
operator: operator == freezed
|
||||||
|
? _value.operator
|
||||||
|
: operator // ignore: cast_nullable_to_non_nullable
|
||||||
|
as LogicOperator,
|
||||||
|
arguments: arguments == freezed
|
||||||
|
? _value.arguments
|
||||||
|
: arguments // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<dynamic>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$_LogicExpressionCopyWith<$Res>
|
||||||
|
implements $LogicExpressionCopyWith<$Res> {
|
||||||
|
factory _$$_LogicExpressionCopyWith(
|
||||||
|
_$_LogicExpression value, $Res Function(_$_LogicExpression) then) =
|
||||||
|
__$$_LogicExpressionCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
$Res call({LogicOperator operator, List<dynamic> arguments});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$_LogicExpressionCopyWithImpl<$Res>
|
||||||
|
extends _$LogicExpressionCopyWithImpl<$Res>
|
||||||
|
implements _$$_LogicExpressionCopyWith<$Res> {
|
||||||
|
__$$_LogicExpressionCopyWithImpl(
|
||||||
|
_$_LogicExpression _value, $Res Function(_$_LogicExpression) _then)
|
||||||
|
: super(_value, (v) => _then(v as _$_LogicExpression));
|
||||||
|
|
||||||
|
@override
|
||||||
|
_$_LogicExpression get _value => super._value as _$_LogicExpression;
|
||||||
|
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? operator = freezed,
|
||||||
|
Object? arguments = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_$_LogicExpression(
|
||||||
|
operator: operator == freezed
|
||||||
|
? _value.operator
|
||||||
|
: operator // ignore: cast_nullable_to_non_nullable
|
||||||
|
as LogicOperator,
|
||||||
|
arguments: arguments == freezed
|
||||||
|
? _value._arguments
|
||||||
|
: arguments // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<dynamic>,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
class _$_LogicExpression extends _LogicExpression {
|
||||||
|
const _$_LogicExpression(
|
||||||
|
{required this.operator, required final List<dynamic> arguments})
|
||||||
|
: _arguments = arguments,
|
||||||
|
super._();
|
||||||
|
|
||||||
|
@override
|
||||||
|
final LogicOperator operator;
|
||||||
|
final List<dynamic> _arguments;
|
||||||
|
@override
|
||||||
|
List<dynamic> get arguments {
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(_arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'LogicExpression(operator: $operator, arguments: $arguments)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(dynamic other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$_LogicExpression &&
|
||||||
|
const DeepCollectionEquality().equals(other.operator, operator) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._arguments, _arguments));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
runtimeType,
|
||||||
|
const DeepCollectionEquality().hash(operator),
|
||||||
|
const DeepCollectionEquality().hash(_arguments));
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
_$$_LogicExpressionCopyWith<_$_LogicExpression> get copyWith =>
|
||||||
|
__$$_LogicExpressionCopyWithImpl<_$_LogicExpression>(this, _$identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _LogicExpression extends LogicExpression {
|
||||||
|
const factory _LogicExpression(
|
||||||
|
{required final LogicOperator operator,
|
||||||
|
required final List<dynamic> arguments}) = _$_LogicExpression;
|
||||||
|
const _LogicExpression._() : super._();
|
||||||
|
|
||||||
|
@override
|
||||||
|
LogicOperator get operator => throw _privateConstructorUsedError;
|
||||||
|
@override
|
||||||
|
List<dynamic> get arguments => throw _privateConstructorUsedError;
|
||||||
|
@override
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$$_LogicExpressionCopyWith<_$_LogicExpression> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
138
lib/utils/logic_operators.dart
Normal file
138
lib/utils/logic_operators.dart
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
abstract class LogicOperator {
|
||||||
|
bool apply(List<bool> inputs);
|
||||||
|
List<String> get representations;
|
||||||
|
String get defaultRepresentation => representations[0];
|
||||||
|
bool fromRepresentation(String repr) => representations.contains(repr);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class ZeroOpLogicOperator extends LogicOperator {}
|
||||||
|
|
||||||
|
class FalseLogicOperator extends ZeroOpLogicOperator {
|
||||||
|
@override
|
||||||
|
bool apply(List<bool> inputs) => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> get representations => ['0'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator==(other) => other is FalseLogicOperator;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => defaultRepresentation.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrueLogicOperator extends ZeroOpLogicOperator {
|
||||||
|
@override
|
||||||
|
bool apply(List<bool> inputs) => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> get representations => ['1'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator==(other) => other is TrueLogicOperator;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => defaultRepresentation.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class OneOpLogicOperator extends LogicOperator {}
|
||||||
|
|
||||||
|
class NotLogicOperator extends OneOpLogicOperator {
|
||||||
|
@override
|
||||||
|
bool apply(List<bool> inputs) => !inputs[0];
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> get representations => const ['~', '!', '¬'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator==(other) => other is NotLogicOperator;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => defaultRepresentation.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class TwoOpLogicOperator extends LogicOperator {}
|
||||||
|
|
||||||
|
class AndLogicOperator extends TwoOpLogicOperator {
|
||||||
|
@override
|
||||||
|
bool apply(List<bool> inputs) => inputs[0] && inputs[1];
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> get representations => const ['&', '∧'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator==(other) => other is AndLogicOperator;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => defaultRepresentation.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrLogicOperator extends TwoOpLogicOperator {
|
||||||
|
@override
|
||||||
|
bool apply(List<bool> inputs) => inputs[0] || inputs[1];
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> get representations => const ['|', '∨'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator==(other) => other is OrLogicOperator;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => defaultRepresentation.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class XorLogicOperator extends TwoOpLogicOperator {
|
||||||
|
@override
|
||||||
|
bool apply(List<bool> inputs) => inputs[0] != inputs[1];
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> get representations => const ['^', '⊕', '⊻'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator==(other) => other is XorLogicOperator;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => defaultRepresentation.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NandLogicOperator extends TwoOpLogicOperator {
|
||||||
|
@override
|
||||||
|
bool apply(List<bool> inputs) => !(inputs[0] && inputs[1]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> get representations => const ['~&', '!&', '¬&', '~∧', '!∧', '¬∧'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator==(other) => other is NandLogicOperator;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => defaultRepresentation.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NorLogicOperator extends TwoOpLogicOperator {
|
||||||
|
@override
|
||||||
|
bool apply(List<bool> inputs) => !(inputs[0] || inputs[1]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> get representations => const ['~|', '!|', '¬|', '~∨', '!∨', '¬∨'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator==(other) => other is NorLogicOperator;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => defaultRepresentation.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class XnorLogicOperator extends TwoOpLogicOperator {
|
||||||
|
@override
|
||||||
|
bool apply(List<bool> inputs) => inputs[0] == inputs[1];
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> get representations => const ['~^', '!^', '¬^', '~⊕', '!⊕', '¬⊕', '~⊻', '!⊻', '¬⊻'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator==(other) => other is XnorLogicOperator;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => defaultRepresentation.hashCode;
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue