feat(plc4go/bacnet): add primitive character string and date
diff --git a/plc4go/internal/bacnetip/debugging.go b/plc4go/internal/bacnetip/debugging.go
index 5c11427..0136cff 100644
--- a/plc4go/internal/bacnetip/debugging.go
+++ b/plc4go/internal/bacnetip/debugging.go
@@ -24,6 +24,10 @@
 	"regexp"
 )
 
+func Btox(data []byte) string {
+	return hex.EncodeToString(data)
+}
+
 func Xtob(hexString string) ([]byte, error) {
 	compile, err := regexp.Compile("[^0-9a-fA-F]")
 	if err != nil {
diff --git a/plc4go/internal/bacnetip/primitivedata.go b/plc4go/internal/bacnetip/primitivedata.go
index 3d76709..398ac56 100644
--- a/plc4go/internal/bacnetip/primitivedata.go
+++ b/plc4go/internal/bacnetip/primitivedata.go
@@ -22,8 +22,12 @@
 import (
 	"bytes"
 	"cmp"
+	"encoding/binary"
 	"fmt"
+	"regexp"
+	"strconv"
 	"strings"
+	"time"
 
 	"github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model"
 
@@ -256,8 +260,76 @@
 	// TODO: implement me
 }
 
+// TODO: finish
 type Null struct {
-	// TODO: implement me
+	*Atomic[int]
+}
+
+func NewNull(arg Arg) (*Null, error) {
+	b := &Null{}
+	b.Atomic = NewAtomic[int](b)
+
+	if arg == nil {
+		return b, nil
+	}
+	switch arg := arg.(type) {
+	case *Tag:
+		err := b.Decode(arg)
+		if err != nil {
+			return nil, errors.Wrap(err, "error decoding")
+		}
+		return b, nil
+	case bool:
+		if arg {
+			b.value = 1
+		}
+	case *Null:
+		b.value = arg.value
+	case string:
+		switch arg {
+		case "True", "true":
+			b.value = 1
+		case "False", "false":
+		default:
+			return nil, errors.Errorf("invalid string: %s", arg)
+		}
+	default:
+		return nil, errors.Errorf("invalid constructor datatype: %T", arg)
+	}
+
+	return b, nil
+}
+
+func (b *Null) Encode(tag *Tag) {
+	tag.set(NewArgs(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_NULL, b.value, []byte{}))
+}
+
+func (b *Null) Decode(tag *Tag) error {
+	if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_NULL) {
+		return errors.New("Null application tag required")
+	}
+	if tag.tagLVT > 1 {
+		return errors.New("invalid tag value")
+	}
+
+	// get the data
+	if tag.tagLVT == 1 {
+		b.value = 1
+	}
+	return nil
+}
+
+func (b *Null) IsValid(arg any) bool {
+	_, ok := arg.(bool)
+	return ok
+}
+
+func (b *Null) String() string {
+	value := "False"
+	if b.value == 1 {
+		value = "True"
+	}
+	return fmt.Sprintf("Null(%s)", value)
 }
 
 type Boolean struct {
@@ -267,7 +339,6 @@
 func NewBoolean(arg Arg) (*Boolean, error) {
 	b := &Boolean{}
 	b.Atomic = NewAtomic[int](b)
-	b.value = 0 // atomic doesn't like bool
 
 	if arg == nil {
 		return b, nil
@@ -341,6 +412,595 @@
 	return fmt.Sprintf("Boolean(%s)", value)
 }
 
+// TODO: finish
+type Unsigned struct {
+	*Atomic[uint]
+	*CommonMath
+}
+
+func NewUnsigned(arg Arg) (*Unsigned, error) {
+	b := &Unsigned{}
+	b.Atomic = NewAtomic[uint](b)
+
+	if arg == nil {
+		return b, nil
+	}
+	switch arg := arg.(type) {
+	case *Tag:
+		err := b.Decode(arg)
+		if err != nil {
+			return nil, errors.Wrap(err, "error decoding")
+		}
+		return b, nil
+	case bool:
+		if arg {
+			b.value = 1
+		}
+	case *Unsigned:
+		b.value = arg.value
+	case string:
+		switch arg {
+		case "True", "true":
+			b.value = 1
+		case "False", "false":
+		default:
+			return nil, errors.Errorf("invalid string: %s", arg)
+		}
+	default:
+		return nil, errors.Errorf("invalid constructor datatype: %T", arg)
+	}
+
+	return b, nil
+}
+
+func (b *Unsigned) Encode(tag *Tag) {
+	tag.set(NewArgs(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_UNSIGNED_INTEGER, b.value, []byte{}))
+}
+
+func (b *Unsigned) Decode(tag *Tag) error {
+	if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_UNSIGNED_INTEGER) {
+		return errors.New("Unsigned application tag required")
+	}
+	if tag.tagLVT > 1 {
+		return errors.New("invalid tag value")
+	}
+
+	// get the data
+	if tag.tagLVT == 1 {
+		b.value = 1
+	}
+	return nil
+}
+
+func (b *Unsigned) IsValid(arg any) bool {
+	_, ok := arg.(bool)
+	return ok
+}
+
+func (b *Unsigned) String() string {
+	value := "False"
+	if b.value == 1 {
+		value = "True"
+	}
+	return fmt.Sprintf("Unsigned(%s)", value)
+}
+
+// TODO: finish
+type Unsigned8 struct {
+	*Atomic[uint8]
+}
+
+func NewUnsigned8(arg Arg) (*Unsigned8, error) {
+	b := &Unsigned8{}
+	b.Atomic = NewAtomic[uint8](b)
+
+	if arg == nil {
+		return b, nil
+	}
+	switch arg := arg.(type) {
+	case *Tag:
+		err := b.Decode(arg)
+		if err != nil {
+			return nil, errors.Wrap(err, "error decoding")
+		}
+		return b, nil
+	case bool:
+		if arg {
+			b.value = 1
+		}
+	case *Unsigned8:
+		b.value = arg.value
+	case string:
+		switch arg {
+		case "True", "true":
+			b.value = 1
+		case "False", "false":
+		default:
+			return nil, errors.Errorf("invalid string: %s", arg)
+		}
+	default:
+		return nil, errors.Errorf("invalid constructor datatype: %T", arg)
+	}
+
+	return b, nil
+}
+
+func (b *Unsigned8) Encode(tag *Tag) {
+	tag.set(NewArgs(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_UNSIGNED_INTEGER, b.value, []byte{}))
+}
+
+func (b *Unsigned8) Decode(tag *Tag) error {
+	if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_UNSIGNED_INTEGER) {
+		return errors.New("Unsigned8 application tag required")
+	}
+	if tag.tagLVT > 1 {
+		return errors.New("invalid tag value")
+	}
+
+	// get the data
+	if tag.tagLVT == 1 {
+		b.value = 1
+	}
+	return nil
+}
+
+func (b *Unsigned8) IsValid(arg any) bool {
+	_, ok := arg.(bool)
+	return ok
+}
+
+func (b *Unsigned8) String() string {
+	value := "False"
+	if b.value == 1 {
+		value = "True"
+	}
+	return fmt.Sprintf("Unsigned8(%s)", value)
+}
+
+// TODO: finish
+type Unsigned16 struct {
+	*Atomic[uint16]
+}
+
+func NewUnsigned16(arg Arg) (*Unsigned16, error) {
+	b := &Unsigned16{}
+	b.Atomic = NewAtomic[uint16](b)
+
+	if arg == nil {
+		return b, nil
+	}
+	switch arg := arg.(type) {
+	case *Tag:
+		err := b.Decode(arg)
+		if err != nil {
+			return nil, errors.Wrap(err, "error decoding")
+		}
+		return b, nil
+	case bool:
+		if arg {
+			b.value = 1
+		}
+	case *Unsigned16:
+		b.value = arg.value
+	case string:
+		switch arg {
+		case "True", "true":
+			b.value = 1
+		case "False", "false":
+		default:
+			return nil, errors.Errorf("invalid string: %s", arg)
+		}
+	default:
+		return nil, errors.Errorf("invalid constructor datatype: %T", arg)
+	}
+
+	return b, nil
+}
+
+func (b *Unsigned16) Encode(tag *Tag) {
+	tag.set(NewArgs(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_UNSIGNED_INTEGER, b.value, []byte{}))
+}
+
+func (b *Unsigned16) Decode(tag *Tag) error {
+	if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_UNSIGNED_INTEGER) {
+		return errors.New("Unsigned16 application tag required")
+	}
+	if tag.tagLVT > 1 {
+		return errors.New("invalid tag value")
+	}
+
+	// get the data
+	if tag.tagLVT == 1 {
+		b.value = 1
+	}
+	return nil
+}
+
+func (b *Unsigned16) IsValid(arg any) bool {
+	_, ok := arg.(bool)
+	return ok
+}
+
+func (b *Unsigned16) String() string {
+	value := "False"
+	if b.value == 1 {
+		value = "True"
+	}
+	return fmt.Sprintf("Unsigned16(%s)", value)
+}
+
+// TODO: finish
+type Integer struct {
+	*Atomic[int]
+	*CommonMath
+}
+
+func NewInteger(arg Arg) (*Integer, error) {
+	b := &Integer{}
+	b.Atomic = NewAtomic[int](b)
+
+	if arg == nil {
+		return b, nil
+	}
+	switch arg := arg.(type) {
+	case *Tag:
+		err := b.Decode(arg)
+		if err != nil {
+			return nil, errors.Wrap(err, "error decoding")
+		}
+		return b, nil
+	case bool:
+		if arg {
+			b.value = 1
+		}
+	case *Integer:
+		b.value = arg.value
+	case string:
+		switch arg {
+		case "True", "true":
+			b.value = 1
+		case "False", "false":
+		default:
+			return nil, errors.Errorf("invalid string: %s", arg)
+		}
+	default:
+		return nil, errors.Errorf("invalid constructor datatype: %T", arg)
+	}
+
+	return b, nil
+}
+
+func (b *Integer) Encode(tag *Tag) {
+	tag.set(NewArgs(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_UNSIGNED_INTEGER, b.value, []byte{}))
+}
+
+func (b *Integer) Decode(tag *Tag) error {
+	if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_UNSIGNED_INTEGER) {
+		return errors.New("Integer application tag required")
+	}
+	if tag.tagLVT > 1 {
+		return errors.New("invalid tag value")
+	}
+
+	// get the data
+	if tag.tagLVT == 1 {
+		b.value = 1
+	}
+	return nil
+}
+
+func (b *Integer) IsValid(arg any) bool {
+	_, ok := arg.(bool)
+	return ok
+}
+
+func (b *Integer) String() string {
+	value := "False"
+	if b.value == 1 {
+		value = "True"
+	}
+	return fmt.Sprintf("Integer(%s)", value)
+}
+
+// TODO: finish
+type Real struct {
+	*Atomic[float32]
+	*CommonMath
+}
+
+func NewReal(arg Arg) (*Real, error) {
+	b := &Real{}
+	b.Atomic = NewAtomic[float32](b)
+
+	if arg == nil {
+		return b, nil
+	}
+	switch arg := arg.(type) {
+	case *Tag:
+		err := b.Decode(arg)
+		if err != nil {
+			return nil, errors.Wrap(err, "error decoding")
+		}
+		return b, nil
+	case bool:
+		if arg {
+			b.value = 1
+		}
+	case *Real:
+		b.value = arg.value
+	case string:
+		switch arg {
+		case "True", "true":
+			b.value = 1
+		case "False", "false":
+		default:
+			return nil, errors.Errorf("invalid string: %s", arg)
+		}
+	default:
+		return nil, errors.Errorf("invalid constructor datatype: %T", arg)
+	}
+
+	return b, nil
+}
+
+func (b *Real) Encode(tag *Tag) {
+	tag.set(NewArgs(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_REAL, b.value, []byte{}))
+}
+
+func (b *Real) Decode(tag *Tag) error {
+	if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_REAL) {
+		return errors.New("Real application tag required")
+	}
+	if tag.tagLVT > 1 {
+		return errors.New("invalid tag value")
+	}
+
+	// get the data
+	if tag.tagLVT == 1 {
+		b.value = 1
+	}
+	return nil
+}
+
+func (b *Real) IsValid(arg any) bool {
+	_, ok := arg.(bool)
+	return ok
+}
+
+func (b *Real) String() string {
+	value := "False"
+	if b.value == 1 {
+		value = "True"
+	}
+	return fmt.Sprintf("Real(%s)", value)
+}
+
+// TODO: finish
+type Double struct {
+	*Atomic[float64]
+	*CommonMath
+}
+
+func NewDouble(arg Arg) (*Double, error) {
+	b := &Double{}
+	b.Atomic = NewAtomic[float64](b)
+
+	if arg == nil {
+		return b, nil
+	}
+	switch arg := arg.(type) {
+	case *Tag:
+		err := b.Decode(arg)
+		if err != nil {
+			return nil, errors.Wrap(err, "error decoding")
+		}
+		return b, nil
+	case bool:
+		if arg {
+			b.value = 1
+		}
+	case *Double:
+		b.value = arg.value
+	case string:
+		switch arg {
+		case "True", "true":
+			b.value = 1
+		case "False", "false":
+		default:
+			return nil, errors.Errorf("invalid string: %s", arg)
+		}
+	default:
+		return nil, errors.Errorf("invalid constructor datatype: %T", arg)
+	}
+
+	return b, nil
+}
+
+func (b *Double) Encode(tag *Tag) {
+	tag.set(NewArgs(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_DOUBLE, b.value, []byte{}))
+}
+
+func (b *Double) Decode(tag *Tag) error {
+	if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_DOUBLE) {
+		return errors.New("Double application tag required")
+	}
+	if tag.tagLVT > 1 {
+		return errors.New("invalid tag value")
+	}
+
+	// get the data
+	if tag.tagLVT == 1 {
+		b.value = 1
+	}
+	return nil
+}
+
+func (b *Double) IsValid(arg any) bool {
+	_, ok := arg.(bool)
+	return ok
+}
+
+func (b *Double) String() string {
+	value := "False"
+	if b.value == 1 {
+		value = "True"
+	}
+	return fmt.Sprintf("Double(%s)", value)
+}
+
+// TODO: finish
+type OctetString struct {
+	*Atomic[string]
+	*CommonMath
+}
+
+func NewOctetString(arg Arg) (*OctetString, error) {
+	b := &OctetString{}
+	b.Atomic = NewAtomic[string](b)
+
+	if arg == nil {
+		return b, nil
+	}
+	switch arg := arg.(type) {
+	case *Tag:
+		err := b.Decode(arg)
+		if err != nil {
+			return nil, errors.Wrap(err, "error decoding")
+		}
+		return b, nil
+	case bool:
+		if arg {
+			b.value = "1"
+		}
+	case *OctetString:
+		b.value = arg.value
+	case string:
+		switch arg {
+		case "True", "true":
+			b.value = "1"
+		case "False", "false":
+		default:
+			return nil, errors.Errorf("invalid string: %s", arg)
+		}
+	default:
+		return nil, errors.Errorf("invalid constructor datatype: %T", arg)
+	}
+
+	return b, nil
+}
+
+func (b *OctetString) Encode(tag *Tag) {
+	tag.set(NewArgs(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_OCTET_STRING, b.value, []byte{}))
+}
+
+func (b *OctetString) Decode(tag *Tag) error {
+	if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_OCTET_STRING) {
+		return errors.New("OctetString application tag required")
+	}
+	if tag.tagLVT > 1 {
+		return errors.New("invalid tag value")
+	}
+
+	// get the data
+	if tag.tagLVT == 1 {
+		b.value = "1"
+	}
+	return nil
+}
+
+func (b *OctetString) IsValid(arg any) bool {
+	_, ok := arg.(bool)
+	return ok
+}
+
+func (b *OctetString) String() string {
+	value := "False"
+	if b.value == "1" {
+		value = "True"
+	}
+	return fmt.Sprintf("OctetString(%s)", value)
+}
+
+type CharacterString struct {
+	*Atomic[string]
+	*CommonMath
+
+	strEncoding byte
+	strValue    []byte
+}
+
+func NewCharacterString(arg Arg) (*CharacterString, error) {
+	c := &CharacterString{}
+	c.Atomic = NewAtomic[string](c)
+
+	if arg == nil {
+		return c, nil
+	}
+	switch arg := arg.(type) {
+	case *Tag:
+		err := c.Decode(arg)
+		if err != nil {
+			return nil, errors.Wrap(err, "error decoding")
+		}
+		return c, nil
+	case string:
+		c.value = arg
+		c.strValue = []byte(c.value)
+	case *CharacterString:
+		c.value = arg.value
+		c.strEncoding = arg.strEncoding
+		c.strValue = arg.strValue
+	default:
+		return nil, errors.Errorf("invalid constructor datatype: %T", arg)
+	}
+
+	return c, nil
+}
+
+func (c *CharacterString) Encode(tag *Tag) {
+	tag.setAppData(uint(model.BACnetDataType_CHARACTER_STRING), append([]byte{c.strEncoding}, c.strValue...))
+}
+
+func (c *CharacterString) Decode(tag *Tag) error {
+	if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_CHARACTER_STRING) {
+		return errors.New("CharacterString application tag required")
+	}
+	if len(tag.tagData) == 0 {
+		return errors.New("invalid tag length")
+	}
+
+	tagData := tag.tagData
+
+	// extract the data
+	c.strEncoding = tagData[0]
+	c.strValue = tagData[1:]
+
+	// normalize the value
+	switch c.strEncoding {
+	case 0:
+		c.value = string(c.strValue)
+	case 3: //utf_32be
+		panic("implement me") // TODO: implement me
+	case 4: //utf_16be
+		panic("implement me") // TODO: implement me
+	case 5: //latin_1
+		panic("implement me") // TODO: implement me
+	default:
+		c.value = fmt.Sprintf("### unknown encoding: %d ###", c.strEncoding)
+	}
+
+	return nil
+}
+
+func (c *CharacterString) IsValid(arg any) bool {
+	_, ok := arg.(bool)
+	return ok
+}
+
+func (c *CharacterString) String() string {
+	return fmt.Sprintf("CharacterString(%d,X'%s')", c.strEncoding, Btox(c.strValue))
+}
+
 // BitStringExtension can be used to inherit from BitString
 type BitStringExtension interface {
 	fmt.Stringer
@@ -495,3 +1155,408 @@
 	// bundle it together
 	return fmt.Sprintf("BitString(%v)", strings.Join(valueList, ","))
 }
+
+// TODO: implement me
+type Enumerated struct {
+	*Atomic[int] // TODO: implement properly
+
+	Value []bool
+}
+
+func NewEnumerated(arg ...any) (*Enumerated, error) {
+	panic("not implemented")
+}
+
+func (b *Enumerated) Decode(tag *Tag) error {
+	if tag.GetTagClass() != model.TagClass_APPLICATION_TAGS || tag.GetTagNumber() != uint(TagEnumeratedAppTag) {
+		return errors.New("bit string application tag required")
+	}
+	if len(tag.GetTagData()) == 0 {
+		return errors.New("invalid tag length")
+	}
+	// extract the number of unused bits
+	unused := tag.tagData[0]
+
+	// extract the data
+	data := make([]bool, 0)
+	for _, x := range tag.tagData[1:] {
+		for i := range 8 {
+			if (x & (1 << (7 - i))) != 0 {
+				data = append(data, true)
+			} else {
+				data = append(data, false)
+			}
+		}
+	}
+
+	// trim off the unused bits
+	if unused != 0 && unused != 8 {
+		b.Value = data[:len(data)-int(unused)]
+	} else {
+		b.Value = data
+	}
+	return nil
+}
+
+func (b *Enumerated) Encode(tag *Tag) {
+	used := len(b.Value) % 8
+	unused := 8 - used
+	if unused == 8 {
+		unused = 0
+	}
+
+	// start with the number of unused bits
+	data := []byte{byte(unused)}
+
+	// build and append each packed octet
+	bits := append(b.Value, make([]bool, unused)...)
+	for i := range len(bits) / 8 {
+		i = i * 8
+		x := byte(0)
+		for j := range 8 {
+			bit := bits[i+j]
+			bitValue := byte(0)
+			if bit {
+				bitValue = 1
+			}
+			x |= bitValue << (7 - j)
+		}
+		data = append(data, x)
+	}
+
+	tag.setAppData(uint(model.BACnetDataType_BIT_STRING), data)
+}
+
+func (b *Enumerated) String() string {
+	// flip the bit names
+	bitNames := map[int]string{}
+
+	// build a list of values and/or names
+	var valueList []string
+	for index, value := range b.Value {
+		if name, ok := bitNames[index]; ok {
+			if value == true {
+				valueList = append(valueList, name)
+			} else {
+				valueList = append(valueList, "!"+name)
+			}
+		} else {
+			if value {
+				valueList = append(valueList, "1")
+			} else {
+				valueList = append(valueList, "0")
+			}
+		}
+	}
+
+	// bundle it together
+	return fmt.Sprintf("Enumerated(%v)", strings.Join(valueList, ","))
+}
+
+const _mm = `(?P<month>0?[1-9]|1[0-4]|odd|even|255|[*])`
+const _dd = `(?P<day>[0-3]?\d|last|odd|even|255|[*])`
+const _yy = `(?P<year>\d{2}|255|[*])`
+const _yyyy = `(?P<year>\d{4}|255|[*])`
+const _dow = `(?P<dow>[1-7]|mon|tue|wed|thu|fri|sat|sun|255|[*])`
+
+var _special_mon = map[string]int{"*": 255, "odd": 13, "even": 14, "": 255}
+var _special_mon_inv = map[int]string{255: "*", 13: "odd", 14: "even"}
+
+var _special_day = map[string]int{"*": 255, "last": 32, "odd": 33, "even": 34, "": 255}
+var _special_day_inv = map[int]string{255: "*", 32: "last", 33: "odd", 34: "even"}
+
+var _special_dow = map[string]int{"*": 255, "mon": 1, "tue": 2, "wed": 3, "thu": 4, "fri": 5, "sat": 6, "sun": 7}
+var _special_dow_inv = map[int]string{255: "*", 1: "mon", 2: "tue", 3: "wed", 4: "thu", 5: "fri", 6: "sat", 7: "sun"}
+
+// Create a composite pattern and compile it.
+func _merge(args ...string) *regexp.Regexp {
+	return regexp.MustCompile(`^` + strings.Join(args, `[/-]`) + `(?:\s+` + _dow + `)?$`)
+}
+
+// make a list of compiled patterns
+var _date_patterns = []*regexp.Regexp{
+	_merge(_yyyy, _mm, _dd),
+	_merge(_mm, _dd, _yyyy),
+	_merge(_dd, _mm, _yyyy),
+	_merge(_yy, _mm, _dd),
+	_merge(_mm, _dd, _yy),
+	_merge(_dd, _mm, _yy),
+}
+
+type DateTuple struct {
+	Year      int
+	Month     int
+	Day       int
+	DayOfWeek int
+}
+
+type Date struct {
+	*Atomic[int64]
+
+	year      int
+	month     int
+	day       int
+	dayOfWeek int
+}
+
+func NewDate(arg Arg, args Args) (*Date, error) {
+	d := &Date{}
+	d.Atomic = NewAtomic[int64](d)
+	year := 255
+	if len(args) > 0 {
+		year = args[0].(int)
+	}
+	if year >= 1900 {
+		year = year - 1900
+	}
+	d.year = year
+	month := 0xff
+	if len(args) > 1 {
+		month = args[1].(int)
+	}
+	d.month = month
+	day := 0xff
+	if len(args) > 2 {
+		day = args[2].(int)
+	}
+	d.day = day
+	dayOfWeek := 0xff
+	if len(args) > 3 {
+		dayOfWeek = args[3].(int)
+	}
+	d.dayOfWeek = dayOfWeek
+
+	if arg == nil {
+		return d, nil
+	}
+	switch arg := arg.(type) {
+	case *Tag:
+		err := d.Decode(arg)
+		if err != nil {
+			return nil, errors.Wrap(err, "error decoding")
+		}
+		return d, nil
+	case DateTuple:
+		d.year, d.month, d.day, d.dayOfWeek = arg.Year, arg.Month, arg.Day, arg.DayOfWeek
+		var tempTime time.Time
+		tempTime.AddDate(d.year, d.month, d.day)
+		d.value = tempTime.UnixNano() - (time.Time{}.UnixNano()) // TODO: check this
+	case string:
+		// lower case everything
+		arg = strings.ToLower(arg)
+
+		// make a list of the contents from matching patterns
+		matches := [][]string{}
+		for _, p := range _date_patterns {
+			if p.MatchString(arg) {
+				groups := combined_pattern.FindStringSubmatch(arg)
+				matches = append(matches, groups[1:])
+			}
+		}
+		if len(matches) == 0 {
+			return nil, errors.New("unmatched")
+		}
+
+		var match []string
+		if len(matches) == 1 {
+			match = matches[0]
+		} else {
+			// check to see if they really are the same
+			panic("what to do here")
+		}
+
+		// extract the year and normalize
+		matchedYear := match[0]
+		if matchedYear == "*" || matchedYear == "" {
+			year = 0xff
+		} else {
+			yearParse, err := strconv.ParseInt(matchedYear, 10, 64)
+			if err != nil {
+				return nil, errors.Wrap(err, "error parsing year")
+			}
+			year = int(yearParse)
+			if year == 0xff {
+				return d, nil
+			}
+			if year < 35 {
+				year += 2000
+			} else if year < 100 {
+				year += 1900
+			} else if year < 1900 {
+				return nil, errors.New("invalid year")
+			}
+		}
+
+		// extract the month and normalize
+		matchedmonth := match[0]
+		if specialMonth, ok := _special_mon[matchedmonth]; ok {
+			month = specialMonth
+		} else {
+			monthParse, err := strconv.ParseInt(matchedmonth, 10, 64)
+			if err != nil {
+				return nil, errors.Wrap(err, "error parsing month")
+			}
+			month = int(monthParse)
+			if month == 0xff {
+				return d, nil
+			}
+			if month == 0 || month > 14 {
+				return nil, errors.New("invalid month")
+			}
+		}
+
+		// extract the day and normalize
+		matchedday := match[0]
+		if specialday, ok := _special_day[matchedday]; ok {
+			day = specialday
+		} else {
+			dayParse, err := strconv.ParseInt(matchedday, 10, 64)
+			if err != nil {
+				return nil, errors.Wrap(err, "error parsing day")
+			}
+			day = int(dayParse)
+			if day == 0xff {
+				return d, nil
+			}
+			if day == 0 || day > 34 {
+				return nil, errors.New("invalid day")
+			}
+		}
+
+		// extract the dayOfWeek and normalize
+		matcheddayOfWeek := match[0]
+		if specialdayOfWeek, ok := _special_dow[matcheddayOfWeek]; ok {
+			dayOfWeek = specialdayOfWeek
+		} else if matcheddayOfWeek == "" {
+			return d, nil
+		} else {
+			dayOfWeekParse, err := strconv.ParseInt(matcheddayOfWeek, 10, 64)
+			if err != nil {
+				return nil, errors.Wrap(err, "error parsing dayOfWeek")
+			}
+			dayOfWeek = int(dayOfWeekParse)
+			if dayOfWeek == 0xff {
+				return d, nil
+			}
+			if dayOfWeek > 7 {
+				return nil, errors.New("invalid dayOfWeek")
+			}
+		}
+
+		// year becomes the correct octet
+		if year != 0xff {
+			year -= 1900
+		}
+
+		// save the value
+		d.year = year
+		d.month = month
+		d.day = day
+		d.dayOfWeek = dayOfWeek
+
+		var tempTime time.Time
+		tempTime.AddDate(year, month, day)
+		d.value = tempTime.UnixNano() - (time.Time{}.UnixNano()) // TODO: check this
+
+		// calculate the day of the week
+		if dayOfWeek == 0 {
+			d.calcDayOfWeek()
+		}
+	case *Date:
+		d.value = arg.value
+		d.year = arg.year
+		d.month = arg.month
+		d.day = arg.day
+		d.dayOfWeek = arg.dayOfWeek
+	case float32:
+		d.now(arg)
+	default:
+		return nil, errors.Errorf("invalid constructor datatype: %T", arg)
+	}
+
+	return d, nil
+}
+
+func (d *Date) GetTupleValue() (year int, month int, day int, dayOfWeek int) {
+	return d.year, d.month, d.day, d.dayOfWeek
+}
+
+func (d *Date) calcDayOfWeek() {
+	year, month, day, dayOfWeek := d.year, d.month, d.day, d.dayOfWeek
+
+	// assume the worst
+	dayOfWeek = 255
+
+	// check for special values
+	if year == 255 {
+		return
+	} else if _, ok := _special_mon_inv[month]; ok {
+		return
+	} else if _, ok := _special_day_inv[month]; ok {
+		return
+	} else {
+		var today time.Time
+		today = time.Date(year+1900, time.Month(month), day, 0, 0, 0, 0, time.UTC)
+		panic(today) // TODO: implement me
+	}
+
+	// put it back together
+	d.year = year
+	d.month = month
+	d.day = day
+	d.dayOfWeek = dayOfWeek
+}
+
+func (d *Date) now(arg float32) {
+	panic("implement me") // TODO
+}
+
+func (d *Date) Encode(tag *Tag) {
+	var b []byte
+	binary.BigEndian.AppendUint64(b, uint64(d.value))
+	tag.setAppData(uint(model.BACnetDataType_DATE), b)
+}
+
+func (d *Date) Decode(tag *Tag) error {
+	if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_DATE) {
+		return errors.New("Date application tag required")
+	}
+	if len(tag.tagData) != 4 {
+		return errors.New("invalid tag length")
+	}
+
+	arg := tag.tagData
+	year, month, day, dayOfWeek := arg[0], arg[1], arg[2], arg[3]
+	var tempTime time.Time
+	tempTime.AddDate(int(year), int(month), int(day))
+	d.value = tempTime.UnixNano() - (time.Time{}.UnixNano()) // TODO: check this
+	d.year, d.month, d.day, d.dayOfWeek = int(year), int(month), int(day), int(dayOfWeek)
+	return nil
+}
+
+func (d *Date) IsValid(arg any) bool {
+	_, ok := arg.(bool)
+	return ok
+}
+
+func (d *Date) String() string {
+	year, month, day, dayOfWeek := d.year, d.month, d.day, d.dayOfWeek
+	yearStr := "*"
+	if year != 255 {
+		yearStr = strconv.Itoa(year + 1900)
+	}
+	monthStr := strconv.Itoa(month)
+	if ms, ok := _special_mon_inv[month]; ok {
+		monthStr = ms
+	}
+	dayStr := strconv.Itoa(day)
+	if ms, ok := _special_day_inv[day]; ok {
+		dayStr = ms
+	}
+	dowStr := strconv.Itoa(dayOfWeek)
+	if ms, ok := _special_dow_inv[dayOfWeek]; ok {
+		dowStr = ms
+	}
+
+	return fmt.Sprintf("Date(%s-%s-%s %s)", yearStr, monthStr, dayStr, dowStr)
+}
diff --git a/plc4go/internal/bacnetip/tests/test_primitive_data/test_character_string_test.go b/plc4go/internal/bacnetip/tests/test_primitive_data/test_character_string_test.go
index ed516b5..7fa35a5 100644
--- a/plc4go/internal/bacnetip/tests/test_primitive_data/test_character_string_test.go
+++ b/plc4go/internal/bacnetip/tests/test_primitive_data/test_character_string_test.go
@@ -19,4 +19,134 @@
 
 package test_primitive_data
 
-// TODO: implement me
+import (
+	"github.com/stretchr/testify/require"
+	"testing"
+
+	"github.com/apache/plc4x/plc4go/internal/bacnetip"
+	"github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model"
+
+	"github.com/stretchr/testify/assert"
+)
+
+const foxMessage = "the quick brown fox jumped over the lazy dog"
+
+func CharacterString(arg ...any) *bacnetip.CharacterString {
+	if len(arg) == 0 {
+		CharacterString, err := bacnetip.NewCharacterString(nil)
+		if err != nil {
+			panic(err)
+		}
+		return CharacterString
+	}
+	CharacterString, err := bacnetip.NewCharacterString(arg[0])
+	if err != nil {
+		panic(err)
+	}
+	return CharacterString
+}
+
+// Convert a hex string to a character_string application tag.
+func CharacterStringTag(x string) *bacnetip.Tag {
+	b := xtob(x)
+	tag := Tag(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_CHARACTER_STRING, len(b), b)
+	return tag
+}
+
+// Encode a CharacterString object into a tag.
+func CharacterStringEncode(obj *bacnetip.CharacterString) *bacnetip.Tag {
+	tag := Tag()
+	obj.Encode(tag)
+	return tag
+}
+
+// Decode a CharacterString application tag into a CharacterString.
+func CharacterStringDecode(tag *bacnetip.Tag) *bacnetip.CharacterString {
+	obj := CharacterString(tag)
+
+	return obj
+}
+
+// Pass the value to CharacterString, construct a tag from the hex string,
+//
+//	and compare results of encode and decoding each other.
+func CharacterStringEndec(t *testing.T, v string, x string) {
+	tag := CharacterStringTag(x)
+
+	obj := CharacterString(v)
+
+	assert.Equal(t, tag, CharacterStringEncode(obj))
+	assert.Equal(t, obj, CharacterStringDecode(tag))
+}
+
+func TestCharacterString(t *testing.T) {
+	obj := CharacterString()
+	assert.Equal(t, "", obj.GetValue())
+
+	assert.Panics(t, func() {
+		CharacterString(1)
+	})
+	assert.Panics(t, func() {
+		CharacterString(1.0)
+	})
+}
+
+func TestCharacterStringStr(t *testing.T) {
+	obj := CharacterString("hello")
+	assert.Equal(t, "hello", obj.GetValue())
+	assert.Equal(t, "CharacterString(0,X'68656c6c6f')", obj.String())
+}
+
+func TestCharacterStringStrUnicode(t *testing.T) {
+	obj := CharacterString("hello")
+	assert.Equal(t, "hello", obj.GetValue())
+	assert.Equal(t, "CharacterString(0,X'68656c6c6f')", obj.String())
+}
+
+func TestCharacterStringStrUnicodeWithLatin(t *testing.T) {
+	// some controllers encoding character string mixing latin-1 and utf-8
+	// try to cover those cases without failing
+	b := xtob("0030b043") // zero degress celsius
+	tag := Tag(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_CHARACTER_STRING, len(b), b)
+	obj := CharacterString()
+	err := obj.Decode(tag)
+	require.NoError(t, err)
+	assert.Equal(t, "CharacterString(0,X'30b043')", obj.String())
+
+	assert.Equal(t, "0\xb0C", obj.GetValue())
+}
+
+func TestCharacterStringTag(t *testing.T) {
+	tag := Tag(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_CHARACTER_STRING, 1, xtob("00"))
+	obj := CharacterString(tag)
+	assert.Equal(t, obj.GetValue(), "")
+
+	tag = Tag(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_BOOLEAN, 0, xtob(""))
+	assert.Panics(t, func() {
+		CharacterString(tag)
+	})
+
+	tag = Tag(model.TagClass_CONTEXT_SPECIFIC_TAGS, 0, 1, xtob("ff"))
+	assert.Panics(t, func() {
+		CharacterString(tag)
+	})
+
+	tag = Tag(bacnetip.TagOpeningTagClass, 0)
+	assert.Panics(t, func() {
+		CharacterString(tag)
+	})
+}
+
+func TestCharacterStringCopy(t *testing.T) {
+	obj1 := CharacterString(foxMessage)
+	obj2 := CharacterString(obj1)
+	assert.Equal(t, obj1, obj2)
+}
+
+func TestCharacterStringEndec(t *testing.T) {
+	assert.Panics(t, func() {
+		CharacterString(CharacterStringTag(""))
+	})
+	CharacterStringEndec(t, "", "00")
+	CharacterStringEndec(t, "abc", "00616263")
+}
diff --git a/plc4go/internal/bacnetip/tests/test_primitive_data/test_date_test.go b/plc4go/internal/bacnetip/tests/test_primitive_data/test_date_test.go
index ed516b5..425ce45 100644
--- a/plc4go/internal/bacnetip/tests/test_primitive_data/test_date_test.go
+++ b/plc4go/internal/bacnetip/tests/test_primitive_data/test_date_test.go
@@ -19,4 +19,123 @@
 
 package test_primitive_data
 
-// TODO: implement me
+import (
+	"testing"
+
+	"github.com/apache/plc4x/plc4go/internal/bacnetip"
+	"github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Date(arg ...any) *bacnetip.Date {
+	if len(arg) == 0 {
+		Date, err := bacnetip.NewDate(nil, nil)
+		if err != nil {
+			panic(err)
+		}
+		return Date
+	}
+	Date, err := bacnetip.NewDate(arg[0], nil)
+	if err != nil {
+		panic(err)
+	}
+	return Date
+}
+
+// Convert a hex string to a character_string application tag.
+func DateTag(x string) *bacnetip.Tag {
+	b := xtob(x)
+	tag := Tag(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_DATE, len(b), b)
+	return tag
+}
+
+// Encode a Date object into a tag.
+func DateEncode(obj *bacnetip.Date) *bacnetip.Tag {
+	tag := Tag()
+	obj.Encode(tag)
+	return tag
+}
+
+// Decode a Date application tag into a Date.
+func DateDecode(tag *bacnetip.Tag) *bacnetip.Date {
+	obj := Date(tag)
+
+	return obj
+}
+
+// Pass the value to Date, construct a tag from the hex string,
+//
+//	and compare results of encode and decoding each other.
+func DateEndec(t *testing.T, v string, x string) {
+	tag := DateTag(x)
+
+	obj := Date(v)
+
+	assert.Equal(t, tag, DateEncode(obj))
+	assert.Equal(t, obj, DateDecode(tag))
+}
+
+func TestDate(t *testing.T) {
+	obj := Date()
+	assert.Equal(t, int64(0), obj.GetValue())
+	year, month, day, week := obj.GetTupleValue()
+	assert.Equal(t, []any{0xff, 0xff, 0xff, 0xff}, []any{year, month, day, week})
+
+	assert.Panics(t, func() {
+		Date("some string")
+	})
+}
+
+func TestDateTuple(t *testing.T) {
+	obj := Date(bacnetip.DateTuple{Year: 1, Month: 2, Day: 3, DayOfWeek: 4})
+	year, month, day, week := obj.GetTupleValue()
+	assert.Equal(t, []any{1, 2, 3, 4}, []any{year, month, day, week})
+	assert.Equal(t, "Date(1901-2-3 thu)", obj.String())
+}
+
+func TestDateTag(t *testing.T) {
+	tag := Tag(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_DATE, 4, xtob("01020304"))
+	obj := Date(tag)
+	year, month, day, week := obj.GetTupleValue()
+	assert.Equal(t, []any{1, 2, 3, 4}, []any{year, month, day, week})
+
+	tag = Tag(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_BOOLEAN, 0, xtob(""))
+	assert.Panics(t, func() {
+		Date(tag)
+	})
+
+	tag = Tag(model.TagClass_CONTEXT_SPECIFIC_TAGS, 0, 1, xtob("ff"))
+	assert.Panics(t, func() {
+		Date(tag)
+	})
+
+	tag = Tag(bacnetip.TagOpeningTagClass, 0)
+	assert.Panics(t, func() {
+		Date(tag)
+	})
+}
+
+func TestDateCopy(t *testing.T) {
+	obj1 := Date(bacnetip.DateTuple{Year: 1, Month: 2, Day: 3, DayOfWeek: 4})
+	obj2 := Date(obj1)
+	assert.Equal(t, obj1, obj2)
+}
+
+func TestDateNow(t *testing.T) {
+	// TODO: upstream doesn't tests this either
+}
+
+func TestDateEndec(t *testing.T) {
+	assert.Panics(t, func() {
+		Date(DateTag(""))
+	})
+}
+
+func TestDateArgs(t *testing.T) {
+	tag := Tag()
+	date := Date(nil, 2023, 2, 10)
+	date1 := Date(nil, 123, 2, 10)
+	assert.Equal(t, date, date1)
+	date.Encode(tag)
+}