{"id":37179358,"url":"https://github.com/gurre/si","last_synced_at":"2026-01-14T20:52:35.983Z","repository":{"id":57582481,"uuid":"145090729","full_name":"gurre/si","owner":"gurre","description":"Système international (SI) units for Golang","archived":false,"fork":false,"pushed_at":"2025-04-20T22:10:45.000Z","size":197,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-20T23:22:39.422Z","etag":null,"topics":["golang","iot","measurement","si"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gurre.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-08-17T08:00:09.000Z","updated_at":"2025-04-20T22:10:48.000Z","dependencies_parsed_at":"2022-08-31T00:41:47.244Z","dependency_job_id":null,"html_url":"https://github.com/gurre/si","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/gurre/si","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gurre%2Fsi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gurre%2Fsi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gurre%2Fsi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gurre%2Fsi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gurre","download_url":"https://codeload.github.com/gurre/si/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gurre%2Fsi/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28434500,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T18:57:19.464Z","status":"ssl_error","status_checked_at":"2026-01-14T18:52:48.501Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["golang","iot","measurement","si"],"created_at":"2026-01-14T20:52:35.283Z","updated_at":"2026-01-14T20:52:35.969Z","avatar_url":"https://github.com/gurre.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ch1 align=\"center\"\u003e\n    \u003cimg src=\"https://github.com/gurre/si/blob/master/gopher_si.png\" alt=\"Mascot\" width=\"300\"\u003e\n    \u003cbr /\u003e\n    Système international (SI)\n\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cb\u003eA powerful, type-safe unit conversion library that prevents expensive mistakes\u003c/b\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://godoc.org/github.com/gurre/si\"\u003e\u003cimg src=\"https://godoc.org/github.com/gurre/si?status.svg\" alt=\"GoDoc\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://goreportcard.com/report/github.com/gurre/si\"\u003e\u003cimg src=\"https://goreportcard.com/badge/github.com/gurre/si\" alt=\"Go Report Card\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n## The Problem: Units Lost in Translation\n\nImagine this: Your industrial monitoring system has sensors collecting data from a manufacturing plant. One sensor reports:\n\n```json\n{\n  \"device_id\": \"pump_3\",\n  \"power_milliwatts\": 2000,\n  \"pressure_kpa\": 350,\n  \"flow_liters_per_minute\": 42\n}\n```\n\nThe data travels through your system, gets processed, stored, and visualized. Months later, a new engineer joins the team and creates a power efficiency calculation:\n\n```go\nefficiency := data.power_milliwatts / (data.pressure_kpa * data.flow_liters_per_minute)\n```\n\nEverything seems fine until an alert triggers with impossible values. After hours of debugging, the mistake becomes clear: the units got confused. The frontend was converting pressure to pascals, but the field name still said \"kpa\". Someone refactored the power field to use watts instead of milliwatts but didn't update the field name.\n\nThis is a common pattern: embedding units in field names seems convenient, but units and values become decoupled as data moves through systems, leading to silent, catastrophic failures.\n\n## The Solution: Values With Units\n\nThe SI library takes a different approach. Instead of separating units from values, it keeps them together:\n\n```json\n{\n  \"device_id\": \"pump_3\",\n  \"power\": \"2 W\",\n  \"pressure\": \"350 kPa\",\n  \"flow\": \"42 L/min\"\n}\n```\n\nNow, each value carries its unit, and your code can parse, validate, and convert units automatically:\n\n```go\npower, _ := si.Parse(\"2 W\")\npressure, _ := si.Parse(\"350 kPa\")\nflow, _ := si.Parse(\"42 L/min\") // Automatically converted to m³/s\n\n// Type-safe operations that maintain correct units\nefficiency := power.Div(pressure.Mul(flow))\n\n// Check for valid efficiency (dimensionless)\nif !si.IsDimension(efficiency, si.Dimensionless) {\n    fmt.Println(\"Invalid efficiency calculation\")\n}\n\nfmt.Println(\"Efficiency:\", efficiency) // Properly formatted\n```\n\nUnits remain attached to values throughout their lifecycle, eliminating an entire class of subtle bugs.\n\n## Data Lineage: Units That Tell Their Story\n\nConsider what happens when your system evolves over time:\n\n```\n2022: Sensors report temperatures in Celsius\n2023: New sensors added that report in Fahrenheit\n2024: System standardized on Kelvin for all calculations\n```\n\nWithout proper unit handling, your historical data becomes a minefield. With SI:\n\n```go\n// Temperature readings from different eras, all with units attached\nreadings := []si.Unit{\n    si.Celsius(22.5),    // 2022 sensor\n    si.Fahrenheit(72.6), // 2023 sensor\n    si.Kelvins(295.7),   // 2024 sensor\n}\n\n// All temperatures converted to a standard unit for analysis\nfor _, temp := range readings {\n    // No need to know which era a reading is from\n    // No need to check field names or metadata\n    c, _ := si.ToCelsius(temp)\n    fmt.Printf(\"Temperature: %s (%.2f C)\\n\", temp, c)\n    // Temperature: 295.65 K (22.50 C)\n    // Temperature: 295.7055555555555 K (22.56 C)\n    // Temperature: 295.7 K (22.55 C)\n}\n```\n\nThe unit is part of the data's DNA, preserving its lineage as it flows through your system.\n\n## Backfilling Data: Future-Proof Your History\n\nSix months into production, you realize your pressure calculations need a correction factor. With embedded units in field names, you'd need to:\n\n1. Create new database fields with updated names\n2. Write complex ETL jobs to transform historical data\n3. Update all downstream systems to use the new fields\n4. Maintain documentation explaining the change\n\nWith SI, backfilling becomes trivial:\n\n```go\n// Process historical data, regardless of when it was collected\nfunc processHistoricalReading(reading string) (string, error) {\n  // Parse the reading with its original units\n  pressure, err := si.Parse(reading)\n  if err != nil {\n    return \"\", err\n  }\n  \n  // Apply correction factor without worrying about the original unit\n  correctedPressure := pressure.Mul(si.Scalar(1.03))\n  \n  // Store or return the corrected value, still with proper units\n  return correctedPressure.String(), nil\n}\n\n// Works seamlessly with:\nprocessHistoricalReading(\"350 kPa\")  // From old dataset, 360.5 kPa\nprocessHistoricalReading(\"0.35 MPa\") // From another system, 360.5 kPa\nprocessHistoricalReading(\"50.8 psi\") // From imperial sensors, 360.7614222400001 kPa\n```\n\nYour historical data remains valuable and accurate, regardless of when or how it was collected.\n\n## Decoupling Values from Schemas: Adaptable Data Models\n\nTraditional systems tightly couple units to database schemas:\n\n```go\ntype Reading struct {\n  ValueMillivolts int    `json:\"value_millivolts\"`\n  Timestamp       string `json:\"timestamp\"`\n}\n\n// To add a new unit type, you need schema migration\ntype NewReading struct {\n  ValueMillivolts int    `json:\"value_millivolts\"`\n  ValueKilopascal int    `json:\"value_kilopascal\"` // New field\n  Timestamp       string `json:\"timestamp\"`\n}\n```\n\nWith SI, your data model becomes flexible and future-proof:\n\n```go\ntype Reading struct {\n  Value     si.Unit `json:\"value\"`      // Can hold any unit type\n  Timestamp string  `json:\"timestamp\"`\n}\n\n// Processing is based on the dimension, not the field name\nfunc processReading(r Reading) {\n  switch {\n  case si.IsDimension(r.Value, si.Volt.Dimension):\n    processVoltage(r.Value)\n  case si.IsDimension(r.Value, si.Pascal.Dimension):\n    processPressure(r.Value)\n  case si.IsDimension(r.Value, si.Temperature):\n    processTemperature(r.Value)\n  // Add new physical quantities without changing the schema\n  }\n}\n```\n\nYour system can adapt to new sensor types, unit preferences, and calculation needs—all without schema migrations or code rewrites.\n\n## 🚀 Installation\n\n```bash\ngo get github.com/gurre/si\n```\n\n## 📚 Usage Examples\n\n```go\n// Create units with various representations\ntemp := si.Celsius(25.5)                                      // 298.65 K\ndistance := si.Kilometers(1.5)                                // 1500 m\nflow := si.Meter.Pow(3).Mul(si.Scalar(0.002)).Div(si.Second)  // 2 L/s\n\n// Parse units from strings (e.g., from sensor readings)\npressure, _ := si.Parse(\"101.325 kPa\")  // 101.325 kPa\nvelocity, _ := si.Parse(\"55 km/h\")      // 15.27777777777778 m/s\n\n// Convert between units\nmeters, _ := distance.ConvertTo(si.Meter)\nfmt.Println(meters)  // 1.5 km\n\n// Temperature conversions\ntempF, _ := si.ToFahrenheit(temp)       // 77.9\ntempC, _ := si.ToCelsius(temp)          // 25.5\n\n// Perform calculations with units\npower := pressure.Mul(flow)                     // 202.65 W\nenergy := power.Mul(si.Hours(2))                // 1.45908 MJ\n```\n\n### Real-World IoT Example\n\n```go\n// Define a sensor reading type\ntype SensorReading struct {\n    DeviceID string\n    Value    si.Unit\n}\n\n// Process readings from different sensors\nreadings := []SensorReading{\n    {DeviceID: \"temp-1\", Value: si.Celsius(24.5)},\n    {DeviceID: \"pressure-1\", Value: si.MustParse(\"101.3 kPa\")},\n}\n\n// Type-safe processing based on dimensions\nfor _, reading := range readings {\n    if si.IsDimension(reading.Value, si.Temperature) {\n        tempC, _ := si.ToCelsius(reading.Value)\n        if tempC \u003e 25.0 {\n            fmt.Printf(\"ALERT: High temperature: %.1f C\\n\", tempC)\n        }\n    } else if si.IsDimension(reading.Value, si.Pascal.Dimension) {\n        fmt.Printf(\"Pressure: %.1f kPa\\n\", reading.Value)\n    }\n}\n```\n\n## 🌟 Key Features\n\n- **Parse sensor readings** with different units (temperature, pressure, flow, etc.)\n- **Perform calculations** across different units safely and accurately\n- **Convert between units** without manual conversion factors\n- **Create derived measurements** from multiple sensor inputs\n- **Verify dimensions** to ensure calculations are physically meaningful\n- **Format output values** with appropriate units for reporting and visualization\n\n## 📖 Documentation\n\nFor complete documentation, visit the [GoDoc page](https://godoc.org/github.com/gurre/si).\n\n## ⚡️ Benchmarks\n\nThe benchmark results show that:\n - Basic operations (Mul, Div, Pow, Add, ConvertTo) are very fast (\u003c10ns) and don't allocate memory\n - Parsing and string operations are more expensive (~3μs for Parse, ~90ns for String)\n - End-to-end calculations take longer (~8-11μs) and require more memory allocations\n - Complex real-world scenarios are the most resource-intensive (14-35μs with 130-312 allocations)\n\n```\ngo test -bench=. -benchmem -benchtime=10s ./...\ngoos: darwin\ngoarch: arm64\npkg: github.com/gurre/si\ncpu: Apple M4 Pro\nBenchmarkParse-12                        \t 4174719\t      2835 ns/op\t    7990 B/op\t      31 allocs/op\nBenchmarkMul-12                          \t1000000000\t         3.284 ns/op\t       0 B/op\t       0 allocs/op\nBenchmarkDiv-12                          \t1000000000\t         3.283 ns/op\t       0 B/op\t       0 allocs/op\nBenchmarkPow-12                          \t1000000000\t         7.120 ns/op\t       0 B/op\t       0 allocs/op\nBenchmarkAdd-12                          \t1000000000\t         2.934 ns/op\t       0 B/op\t       0 allocs/op\nBenchmarkConvertTo-12                    \t1000000000\t         2.957 ns/op\t       0 B/op\t       0 allocs/op\nBenchmarkString-12                       \t138894262\t        86.27 ns/op\t      28 B/op\t       2 allocs/op\nBenchmarkComplexCalculation-12           \t1000000000\t         8.772 ns/op\t       0 B/op\t       0 allocs/op\nBenchmarkHydraulicPower-12               \t1000000000\t         3.325 ns/op\t       0 B/op\t       0 allocs/op\nBenchmarkReynoldsNumber-12               \t849424845\t        14.20 ns/op\t       0 B/op\t       0 allocs/op\nBenchmarkVerifyDimension-12              \t1000000000\t         4.576 ns/op\t       0 B/op\t       0 allocs/op\nBenchmarkEndToEndFlowCalculation-12      \t 1000000\t     10917 ns/op\t   30680 B/op\t     118 allocs/op\nBenchmarkEndToEndEnergyCalculation-12    \t 1518381\t      7913 ns/op\t   22096 B/op\t      80 allocs/op\nBenchmarkCelsiusToKelvin-12              \t 4621032\t      2560 ns/op\t    7344 B/op\t      26 allocs/op\nBenchmarkKilometersToMeters-12           \t 4721824\t      2546 ns/op\t    7344 B/op\t      26 allocs/op\nBenchmarkMarshalJSON-12                  \t84430513\t       142.0 ns/op\t      56 B/op\t       4 allocs/op\nBenchmarkPressureConversion-12           \t1000000000\t         3.079 ns/op\t       0 B/op\t       0 allocs/op\nBenchmarkThermodynamicCalculation-12     \t  924319\t     13099 ns/op\t   36720 B/op\t     130 allocs/op\nBenchmarkHeatExchangerDesign-12          \t  587031\t     20535 ns/op\t   58752 B/op\t     208 allocs/op\nBenchmarkPumpingSystemAnalysis-12        \t  739095\t     16010 ns/op\t   44064 B/op\t     156 allocs/op\nBenchmarkElectricalCircuitAnalysis-12    \t  364904\t     32496 ns/op\t   88128 B/op\t     312 allocs/op\n```\n\n# AST-based parser for units\n\n1. The unit system is built around the Unit struct, which contains:\n    - Value: A float64 representing the scalar magnitude of the physical quantity\n    - Dimension: A [7]int array representing the exponents of the 7 SI base dimensions (Length, Mass, Time, Current, Temperature, Substance, Luminosity)\n2. Units are registered in the StandardContext struct, which implements the Context interface. The StandardContext contains:\n    - baseUnits: A map of base unit symbols (e.g., \"m\", \"kg\") to their Unit definitions\n    - derivedUnits: A map of derived unit symbols (e.g., \"N\", \"J\") to their Unit definitions\n    - prefixes: A map of unit prefixes (e.g., \"k\", \"M\") to their scaling factors\n    - sortedPrefixes: A slice of prefixes sorted by length for proper matching\n3. Registration happens in three main methods:\n    - registerBaseUnits(): Registers the 7 SI base units (meter, kilogram, second, ampere, kelvin, mole, candela)\n    - registerDerivedUnits(): Registers derived units (newton, joule, watt, etc.) by combining base units\n    - registerPrefixes(): Registers SI prefixes (kilo, mega, etc.) and binary prefixes (kibi, mebi, etc.)\n4. The entire process is initiated in the NewStandardContext() function, which creates a new context and calls these registration methods.\n5. Unit symbols are resolved via the Resolve() method, which:\n    - Checks for special cases (dimensionless units, gram)\n    - Looks up exact matches in base and derived unit maps\n    - Handles prefixed units by checking if a symbol starts with a known prefix\n    - Returns a Unit with the appropriate value and dimension\n6. Complex unit expressions like \"kgm/s^2\" are parsed using an AST-based parser:\n    - ParseComplexUnit() tokenizes the input and builds an abstract syntax tree\n    - The parser handles identifiers, numbers, parentheses, and operations (multiplication, division, powers)\n    - The AST nodes are then evaluated with the context to produce a final Unit\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgurre%2Fsi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgurre%2Fsi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgurre%2Fsi/lists"}