Skip to content

Commit

Permalink
Add 𝐧𝐞𝐰 functions to get checksums of Technische / Steuerbare Ressour…
Browse files Browse the repository at this point in the history
…ce; Change MaLo-ID Checksum func to not panic but return error (#324)

* Add functions to get checksums of Technische and Steuerbare Ressource

* less panic

* tests are green

* Update bo/netzlokation.go

---------

Co-authored-by: Konstantin <konstantin.klein+github@hochfrequenz.de>
Co-authored-by: konstantin <konstantin.klein@hochfrequenz.de>
  • Loading branch information
3 people authored Sep 20, 2024
1 parent e91cfc1 commit bdd76c8
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 28 deletions.
37 changes: 11 additions & 26 deletions bo/netzlokation.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package bo

import (
"fmt"
"github.com/hochfrequenz/go-bo4e/com"
"github.com/hochfrequenz/go-bo4e/enum/marktrolle"
"github.com/hochfrequenz/go-bo4e/enum/sparte"
"github.com/hochfrequenz/go-bo4e/internal"
"regexp"
"strconv"
"unicode"
)

// NeLo is short for Netzlokation.
// There should be a nelo struct in the future. for now we just add the nelo-id validation

// neloIdRegex is a regex that all Netzlokation-IDs must match: An "E" followed by 9 upper case letters or digits and a trailing checksum
var neloIdRegex = regexp.MustCompile(`^E[A-Z\d]{9}\d{1}$`)
Expand All @@ -19,7 +19,7 @@ var neloIdRegex = regexp.MustCompile(`^E[A-Z\d]{9}\d{1}$`)
var neloIdRegexWithoutChecksum = regexp.MustCompile(`^E[A-Z\d]{9}$`)

// GetNeLoIdCheckSum returns the checksum (11th character of the nelo ID) that matches the first ten characters long provided in neloIdWithoutCheckSum. This is going to crash if the length of the neloIdWithoutCheckSum is <10. Use neloIdWithoutCheckSum + strconv.Itoa(returnValue) to generate a NeLoId
func GetNeLoIdCheckSum(neloIdWithoutCheckSum string) int {
func GetNeLoIdCheckSum(neloIdWithoutCheckSum string) (int, error) {
// Quote from https://bdew-codes.de/Content/Files/Anwdh_2023-01-18-AWH-Identifikatoren-MaKo-Bildungsvorschrift_Version.1.0.pdf chapter 6.2
// > Das ASCII-Verfahren zur Berechnung der Prüfziffer findet bei der Ressourcen-ID und der NeLo-ID Anwendung.
// Verfahren:
Expand All @@ -39,33 +39,18 @@ func GetNeLoIdCheckSum(neloIdWithoutCheckSum string) int {
// Find an online tool for the check here: https://bdew-codes.de/Codenumbers/NetLocationId (click "Prüfziffernrechner" on the right sidebar)
inputMatchesRegex := neloIdRegexWithoutChecksum.MatchString(neloIdWithoutCheckSum)
if !inputMatchesRegex {
panic("You must provide a string that matches ^E[A-Z\\d]{9}$")
return 0, fmt.Errorf("you must provide a string that matches E[A-Z\\d]{9}, but '%s' does not", neloIdWithoutCheckSum)
}
evenSum := 0
oddSum := 0
for index, digitRune := range neloIdWithoutCheckSum[0:10] {
var digit int
if !unicode.IsDigit(digitRune) {
// if the digitRune is a letter, then we du the usual ASCII conversion
digit = int(digitRune) // digit is 65 for digitRune='A'
} else {
//, but if it's a "digit" character, then we use the digits value. einmal mit profis arbeiten
digit = int(digitRune - '0') // digit is 0 for digitRune='0'
}
if index%2 == 0 {
// this is "odd", because BDEW starts counting at 1, so the first index is odd 🙄 einmal mit profis arbeiten
oddSum = oddSum + digit
} else {
evenSum = evenSum + digit
}
checksum, checksumErr := internal.GetChecksum(neloIdWithoutCheckSum)
if checksumErr != nil {
return 0, checksumErr
}
stepD := oddSum + (evenSum * 2)
result := (((stepD/10)+1)*10 - stepD) % 10
resultMatchesRegex := neloIdRegex.MatchString(neloIdWithoutCheckSum + strconv.Itoa(result))
result := neloIdWithoutCheckSum + checksum
resultMatchesRegex := neloIdRegex.MatchString(result)
if !resultMatchesRegex {
panic("This function is broken; And this should never happen")
return 0, fmt.Errorf("this function is broken; And this should never happen")
}
return result
return strconv.Atoi(checksum)
}

// Netzlokation is a minimalistic implementation of the BO Netzlokation. But this small implementation alone, allows use to unmarshall boneycombs that contain Netzlokation-BOs
Expand Down
6 changes: 4 additions & 2 deletions bo/netzlokation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,13 @@ func Test_Serialized_Empty_Netzlokation_Contains_No_Enum_Defaults(t *testing.T)
}

func Test_Get_NeloId_Checksum(t *testing.T) {
actual := bo.GetNeLoIdCheckSum("E113735592")
actual, err := bo.GetNeLoIdCheckSum("E113735592")
then.AssertThat(t, err, is.Nil())
then.AssertThat(t, actual, is.EqualTo(1))
}

func Test_Get_NeloId_Doesnt_Panic(t *testing.T) {
actual := bo.GetNeLoIdCheckSum("E5345G7F6F")
actual, err := bo.GetNeLoIdCheckSum("E5345G7F6F")
then.AssertThat(t, err, is.Nil())
then.AssertThat(t, actual, is.EqualTo(0))
}
46 changes: 46 additions & 0 deletions bo/steuerbareressorce.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,55 @@
package bo

import (
"fmt"
"github.com/hochfrequenz/go-bo4e/enum/steuerkanalsleistungsbeschreibung"
"github.com/hochfrequenz/go-bo4e/internal"
"regexp"
"strconv"
)

// SR-ID is short for Steuerbare Ressource-ID
// srIdRegex is a regex that all Steuerbare Ressourcen-IDs must match: A "C" followed by 9 upper case letters or digits and a trailing checksum
var srIdRegex = regexp.MustCompile(`^C[A-Z\d]{9}\d{1}$`)

// srIdRegexWithoutChecksum is a regex that all Steuerbare Ressourcen-IDs[0:10] must match: A "C" followed by 9 upper case letters or digits BUT WITHOUT A TRAILING CHECKSUM
var srIdRegexWithoutChecksum = regexp.MustCompile(`^C[A-Z\d]{9}$`)

// GetSRIdCheckSum returns the checksum (11th character of the SR ID) that matches the first ten characters long provided in srIdWithoutCheckSum. This is going to crash if the length of the srIdWithoutCheckSum is <10. Use srIdWithoutCheckSum + strconv.Itoa(returnValue) to generate a SR ID
func GetSRIdCheckSum(srIdWithoutCheckSum string) (int, error) {
// Quote from https://bdew-codes.de/Content/Files/Anwdh_2023-01-18-AWH-Identifikatoren-MaKo-Bildungsvorschrift_Version.1.0.pdf chapter 6.2
// > Das ASCII-Verfahren zur Berechnung der Prüfziffer findet bei der Ressourcen-ID und der NeLo-ID Anwendung.
// Verfahren:
// a) Umwandlung der Buchstaben mittels ASCII-Tabelle in Zahlenwerte
// b) Quersumme aller Ziffern in ungerader Position
// c) Quersumme aller Ziffern auf gerader Position multipliziert mit 2
// d) Summe von b) und c)
// e) Differenz von d) zum nächsthöheren Vielfachen von 10 (ergibt sich hier 10, wird die
// Prüfziffer 0 genommen)
// Beispiel: Code: A 1 1 3 7 3 5 5 9 2 PZ
// a) A = 65
// b) 65 + 1 + 7 + 5 + 9 = 87
// c) (1 + 3 + 3 + 5 + 2) * 2 = 28
// d) 87 + 28 = 115
// e) 120 - 115 = 5 => Prüfziffer 5
// Identifikationsnummer: A 1 1 3 7 3 5 5 9 2 5
// Find an online tool for the check here: https://bdew-codes.de/Codenumbers/NetLocationId (click "Prüfziffernrechner" on the right sidebar)
inputMatchesRegex := srIdRegexWithoutChecksum.MatchString(srIdWithoutCheckSum)
if !inputMatchesRegex {
return 0, fmt.Errorf("you must provide a string that matches ^C[A-Z\\d]{9}, but '%s' does not", srIdWithoutCheckSum)
}
checksum, checksumErr := internal.GetChecksum(srIdWithoutCheckSum)
if checksumErr != nil {
return 0, checksumErr
}
result := srIdWithoutCheckSum + checksum
resultMatchesRegex := srIdRegex.MatchString(result)
if !resultMatchesRegex {
return 0, fmt.Errorf("this function is broken; And this should never happen")
}
return strconv.Atoi(checksum)
}

type SteuerbareRessource struct {
Geschaeftsobjekt
SteuerbareRessourceId string `json:"steuerbareRessourceId" validate:"required"` // Identifikationsnummer einer SteuerbareRessource
Expand Down
12 changes: 12 additions & 0 deletions bo/steuerbareressource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,15 @@ func Test_Empty_SteuerbarreRessource_Is_Creatable_Using_BoTyp(t *testing.T) {
func Test_Serialized_Empty_SteuerbarreRessource_Contains_No_Enum_Defaults(t *testing.T) {
assertDoesNotSerializeDefaultEnums(t, bo.NewBusinessObject(botyp.STEUERBARERESSOURCE))
}

func Test_Get_SRId_Checksum(t *testing.T) {
actual, err := bo.GetSRIdCheckSum("C816417ST7")
then.AssertThat(t, err, is.Nil())
then.AssertThat(t, actual, is.EqualTo(7))
}

func Test_Get_SRId_Doesnt_Panic(t *testing.T) {
actual, err := bo.GetSRIdCheckSum("C5345G9F6F")
then.AssertThat(t, err, is.Nil())
then.AssertThat(t, actual, is.EqualTo(0))
}
46 changes: 46 additions & 0 deletions bo/technischeressource.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,61 @@
package bo

import (
"fmt"
"github.com/hochfrequenz/go-bo4e/com"
"github.com/hochfrequenz/go-bo4e/enum/emobilitaetsart"
"github.com/hochfrequenz/go-bo4e/enum/erzeugungsart"
"github.com/hochfrequenz/go-bo4e/enum/speicherart"
"github.com/hochfrequenz/go-bo4e/enum/technischeressourcenutzung"
"github.com/hochfrequenz/go-bo4e/enum/technischeressourceverbrauchsart"
"github.com/hochfrequenz/go-bo4e/enum/waermenutzung"
"github.com/hochfrequenz/go-bo4e/internal"
"regexp"
"strconv"
)

// TR-ID is short for Technische Ressource-ID
// trIdRegex is a regex that all Technische Ressourcen-IDs must match: A "D" followed by 9 upper case letters or digits and a trailing checksum
var trIdRegex = regexp.MustCompile(`^D[A-Z\d]{9}\d{1}$`)

// trIdRegexWithoutChecksum is a regex that all Technische Ressourcen-IDs[0:10] must match: A "D" followed by 9 upper case letters or digits BUT WITHOUT A TRAILING CHECKSUM
var trIdRegexWithoutChecksum = regexp.MustCompile(`^D[A-Z\d]{9}$`)

// GetTRIdCheckSum returns the checksum (11th character of the TR ID) that matches the first ten characters long provided in trIdWithoutCheckSum. This is going to crash if the length of the trIdWithoutCheckSum is <10. Use trIdWithoutCheckSum + strconv.Itoa(returnValue) to generate a TR ID
func GetTRIdCheckSum(trIdWithoutCheckSum string) (int, error) {
// Quote from https://bdew-codes.de/Content/Files/Anwdh_2023-01-18-AWH-Identifikatoren-MaKo-Bildungsvorschrift_Version.1.0.pdf chapter 6.2
// > Das ASCII-Verfahren zur Berechnung der Prüfziffer findet bei der Ressourcen-ID und der NeLo-ID Anwendung.
// Verfahren:
// a) Umwandlung der Buchstaben mittels ASCII-Tabelle in Zahlenwerte
// b) Quersumme aller Ziffern in ungerader Position
// c) Quersumme aller Ziffern auf gerader Position multipliziert mit 2
// d) Summe von b) und c)
// e) Differenz von d) zum nächsthöheren Vielfachen von 10 (ergibt sich hier 10, wird die
// Prüfziffer 0 genommen)
// Beispiel: Code: A 1 1 3 7 3 5 5 9 2 PZ
// a) A = 65
// b) 65 + 1 + 7 + 5 + 9 = 87
// c) (1 + 3 + 3 + 5 + 2) * 2 = 28
// d) 87 + 28 = 115
// e) 120 - 115 = 5 => Prüfziffer 5
// Identifikationsnummer: A 1 1 3 7 3 5 5 9 2 5
// Find an online tool for the check here: https://bdew-codes.de/Codenumbers/NetLocationId (click "Prüfziffernrechner" on the right sidebar)
inputMatchesRegex := trIdRegexWithoutChecksum.MatchString(trIdWithoutCheckSum)
if !inputMatchesRegex {
return 0, fmt.Errorf("you must provide a string that matches ^D[A-Z\\d]{9}, but '%s' does not", trIdWithoutCheckSum)
}
checksum, checksumErr := internal.GetChecksum(trIdWithoutCheckSum)
if checksumErr != nil {
return 0, checksumErr
}
result := trIdWithoutCheckSum + checksum
resultMatchesRegex := trIdRegex.MatchString(result)
if !resultMatchesRegex {
return 0, fmt.Errorf("this function is broken; And this should never happen")
}
return strconv.Atoi(checksum)
}

type TechnischeRessource struct {
Geschaeftsobjekt
TechnischeRessourceId *string `json:"technischeRessourceId" validate:"required"` //Identifikationsnummer einer TechnischeRessource
Expand Down
12 changes: 12 additions & 0 deletions bo/technischeressource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,15 @@ func Test_Empty_TechnischeRessource_Is_Creatable_Using_BoTyp(t *testing.T) {
func Test_Serialized_Empty_TechnischeRessource_Contains_No_Enum_Defaults(t *testing.T) {
assertDoesNotSerializeDefaultEnums(t, bo.NewBusinessObject(botyp.TECHNISCHERESSOURCE))
}

func Test_Get_TRId_Checksum(t *testing.T) {
actual, err := bo.GetTRIdCheckSum("D113735592")
then.AssertThat(t, err, is.Nil())
then.AssertThat(t, actual, is.EqualTo(2))
}

func Test_Get_TRId_Doesnt_Panic(t *testing.T) {
actual, err := bo.GetTRIdCheckSum("D5345G7F7F")
then.AssertThat(t, err, is.Nil())
then.AssertThat(t, actual, is.EqualTo(0))
}
35 changes: 35 additions & 0 deletions internal/checksum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package internal

import (
"fmt"
"strconv"
"unicode"
)

// GetChecksum calculates the checksum of a (Netzlokation/Technische Ressource/Steuerbare Ressource) ID for a given 10-digit ID string (that does not contain a without checksum yet)
func GetChecksum(idWithoutChecksum string) (string, error) {
if len(idWithoutChecksum) != 10 {
return "", fmt.Errorf("You must provide a string that is 10 characters long but '%s' is %d characters long", idWithoutChecksum, len(idWithoutChecksum))
}
evenSum := 0
oddSum := 0
for index, digitRune := range idWithoutChecksum[0:10] {
var digit int
if !unicode.IsDigit(digitRune) {
// if the digitRune is a letter, then we du the usual ASCII conversion
digit = int(digitRune) // digit is 65 for digitRune='A'
} else {
//, but if it's a "digit" character, then we use the digits value. einmal mit profis arbeiten
digit = int(digitRune - '0') // digit is 0 for digitRune='0'
}
if index%2 == 0 {
// this is "odd", because BDEW starts counting at 1, so the first index is odd 🙄 einmal mit profis arbeiten
oddSum = oddSum + digit
} else {
evenSum = evenSum + digit
}
}
stepD := oddSum + (evenSum * 2)
result := (((stepD/10)+1)*10 - stepD) % 10
return strconv.Itoa(result), nil
}

0 comments on commit bdd76c8

Please sign in to comment.