# Coordinate Sanitizer

[![npm version](https://badge.fury.io/js/coordinate-sanitizer.svg)](https://badge.fury.io/js/coordinate-sanitizer)
[![npm downloads](https://img.shields.io/npm/dm/coordinate-sanitizer.svg)](https://www.npmjs.com/package/coordinate-sanitizer)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Node.js Support](https://img.shields.io/node/v/coordinate-sanitizer.svg)](https://nodejs.org)

<p align="center">
    <img src="https://raw.githubusercontent.com/Francescodib/coordinate-sanitizer/main/assets/CoordinateSanitizerLogo_web.jpg"
  alt="coordinate-sanitizer" width="100%" />
</p>

A flexible and robust JavaScript library for parsing and sanitizing astronomical coordinates. Handles various coordinate formats and converts them to standardized formats suitable for astronomical applications.

## Installation

```bash
npm install coordinate-sanitizer
```

[![NPM](https://nodei.co/npm/coordinate-sanitizer.png?downloads=true&downloadRank=true&stars=true)](https://www.npmjs.com/package/coordinate-sanitizer)

## Features

- **Multiple Input Formats**: Supports HMS/DMS, decimal, and compact coordinate formats
- **Flexible Output**: Configurable output formats (Aladin, decimal, HMS/DMS)
- **Range Validation**: Optional validation of RA (0-24h) and DEC (-90°/+90°) ranges
- **Security**: Built-in protection against malicious input
- **Object Recognition**: Automatically detects and passes through astronomical object names
- **Unicode Support**: Handles various Unicode symbols and separators
- **High Performance**: Optimized for batch processing
- **Zero Dependencies**: Lightweight with no external dependencies
- **Universal**: Works in Node.js and browsers
- **TypeScript Support**: Includes complete TypeScript definitions

## Quick Start

```javascript
const CoordinateSanitizer = require('coordinate-sanitizer');

const sanitizer = new CoordinateSanitizer();

// Parse various coordinate formats
const result1 = sanitizer.sanitizeCoordinates('12h 34m 56s, +12° 34\' 56"');
console.log(result1.coordinates); // "12 34 56.000, +12 34 56.000"

const result2 = sanitizer.sanitizeCoordinates('M31');
console.log(result2.coordinates); // "M31" (passed through as object name)

const result3 = sanitizer.sanitizeCoordinates('123.456, -12.345');
console.log(result3.coordinates); // "08 13 49.440, -12 20 42.000"
```

## Package Information

| Property | Value |
|----------|--------|
| **Package Name** | [`coordinate-sanitizer`](https://www.npmjs.com/package/coordinate-sanitizer) |
| **Version** | ![npm version](https://img.shields.io/npm/v/coordinate-sanitizer.svg) |
| **Weekly Downloads** | ![npm downloads](https://img.shields.io/npm/dw/coordinate-sanitizer.svg) |
| **Bundle Size** | ![npm bundle size](https://img.shields.io/bundlephobia/min/coordinate-sanitizer.svg) |
| **License** | ![License](https://img.shields.io/npm/l/coordinate-sanitizer.svg) |
| **Node.js Support** | ![Node.js](https://img.shields.io/node/v/coordinate-sanitizer.svg) |

## Supported Input Formats

### Coordinate Formats
- **HMS/DMS**: `12h 34m 56.78s, +12° 34' 56.78"`
- **Colon separated**: `12:34:56.78, +12:34:56.78`
- **Decimal degrees**: `123.456, -12.345` (RA in degrees by default; set `raDecimalUnit: 'hours'` to interpret it as hours)
- **Compact**: `123456, -123456`
- **Space separated**: `12 34 56.7 -45 12 34.5`
- **Labeled**: `RA 12 34 56.7 DEC -45 12 34.5`, `RA: 12:34:56, DEC: -45:12:34`
- **Mixed formats**: `12h 34m 56s, +12:34:56`

### Object Names
- **Messier objects**: `M31`, `M42`
- **NGC objects**: `NGC 1234`, `NGC 7000`
- **IC objects**: `IC 1396`
- **Other catalogs**: `HD 209458`, `HIP 27989`, `SAO 123456`
- **Named stars**: `Polaris`, `Vega`, `51 Eri`

### Separators
Supports multiple separators between RA and DEC:
- Comma: `,`
- Semicolon: `;`
- Middle dot: `·`
- Bullet: `•`

## API Reference

### Constructor

```javascript
const sanitizer = new CoordinateSanitizer(options);
```

**Options:**
- `outputFormat` (string): Output format - `'aladin'`, `'decimal'`, `'hms-dms'` (default: `'aladin'`)
- `precision` (number): Decimal precision for output (default: `6`)
- `validateRanges` (boolean): Enable range validation (default: `true`)
- `strictMode` (boolean): Enable strict parsing mode (default: `false`). In strict mode, compact 6-digit formats and space-separated coordinates without an explicit separator are rejected; only unambiguous formats are accepted (HMS/DMS with markers, colon-separated, decimal, labeled).
- `raDecimalUnit` (string): Unit of a bare decimal RA input and of decimal RA output - `'degrees'` or `'hours'` (default: `'degrees'`, the astronomical convention for decimal coordinates)

Note: regardless of `validateRanges`, sexagesimal minutes and seconds must always be in the 0-59 range; `12h 99m 00s` is rejected as malformed notation.

### Methods

#### `sanitizeCoordinates(input)`

Main method for sanitizing coordinates.

**Parameters:**
- `input` (string): Input coordinate string

**Returns:**
```javascript
{
  isValid: boolean,      // Whether the input was successfully parsed
  coordinates: string,   // Sanitized coordinate string
  error: string|null,    // Error message if parsing failed
  metadata: {            // Additional information about the parsing
    inputFormat: string, // 'coordinates', 'object-name', 'already-valid'
    outputFormat: string,// Output format used
    ra: object,          // RA parsing details (if coordinates)
    dec: object          // DEC parsing details (if coordinates)
  }
}
```

#### `parse(input)`

Parses coordinates and returns plain numeric values, without string formatting. Returns `null` when the input is invalid or an object name.

```javascript
const sanitizer = new CoordinateSanitizer();
const parsed = sanitizer.parse('05h 35m 17.30s, -05° 23\' 28.00"');
console.log(parsed);
// { raHours: 5.588139, raDegrees: 83.822083, decDegrees: -5.391111 }

sanitizer.parse('M31'); // null (object name, no numeric coordinates)
```

#### Static Methods

##### `getSupportedFormats()`

Returns information about supported input and output formats.

##### `createPreset(preset)`

Creates a sanitizer with predefined configurations:
- `'aladin'`: Aladin format with range validation
- `'decimal'`: Decimal format with high precision
- `'loose'`: Aladin format without range validation
- `'strict'`: Aladin format with strict parsing and validation

Throws an `Error` if an unknown preset name is provided.

## Usage Examples

### Different Output Formats

```javascript
// Aladin format (default)
const aladinSanitizer = new CoordinateSanitizer({ outputFormat: 'aladin' });
const result1 = aladinSanitizer.sanitizeCoordinates('12h 34m 56s, +12° 34\' 56"');
console.log(result1.coordinates); // "12 34 56.000, +12 34 56.000"

// Decimal format (degrees for both RA and DEC)
const decimalSanitizer = new CoordinateSanitizer({ outputFormat: 'decimal' });
const result2 = decimalSanitizer.sanitizeCoordinates('12h 34m 56s, +12° 34\' 56"');
console.log(result2.coordinates); // "188.733333, 12.582222"

// HMS/DMS format (decimal input is in degrees: RA 187.5° = 12.5h)
const hmsSanitizer = new CoordinateSanitizer({ outputFormat: 'hms-dms' });
const result3 = hmsSanitizer.sanitizeCoordinates('187.5, 12.5');
console.log(result3.coordinates); // "12h 30m 00.000s, +12° 30' 00.000""
```

### Using Presets

```javascript
// Quick setup with presets
const aladinSanitizer  = CoordinateSanitizer.createPreset('aladin');
const decimalSanitizer = CoordinateSanitizer.createPreset('decimal');
const looseSanitizer   = CoordinateSanitizer.createPreset('loose');
const strictSanitizer  = CoordinateSanitizer.createPreset('strict');

const result = aladinSanitizer.sanitizeCoordinates('12h 34m 56s, +12° 34\' 56"');

// createPreset throws for unknown names
try {
  CoordinateSanitizer.createPreset('unknown');
} catch (e) {
  console.error(e.message); // "Unknown preset: "unknown". Available presets: aladin, decimal, loose, strict"
}
```

### Strict Mode

Strict mode accepts only unambiguous coordinate formats and requires an explicit separator between RA and DEC:

```javascript
const strict = new CoordinateSanitizer({ strictMode: true });

// Accepted: HMS/DMS with markers, colon-separated, decimal
strict.sanitizeCoordinates('12h 34m 56s, +12° 34\' 56"').isValid; // true
strict.sanitizeCoordinates('12:34:56, +12:34:56').isValid;         // true
strict.sanitizeCoordinates('12.5, -45.75').isValid;                // true

// Rejected: compact format, space-separated without explicit separator
strict.sanitizeCoordinates('123456, -123456').isValid;       // false
strict.sanitizeCoordinates('12 34 56 -45 12 34').isValid;    // false
```

### Range Validation

```javascript
const sanitizer = new CoordinateSanitizer({ validateRanges: true });

// Valid coordinates
const valid = sanitizer.sanitizeCoordinates('12h 00m 00s, +45° 00\' 00"');
console.log(valid.isValid); // true

// Invalid RA (> 24h)
const invalid = sanitizer.sanitizeCoordinates('25h 00m 00s, +45° 00\' 00"');
console.log(invalid.isValid); // false
console.log(invalid.error); // "RA out of range: 25 (must be 0-24 hours)"
```

### Batch Processing

```javascript
const sanitizer = new CoordinateSanitizer();

const inputs = [
  'M31',
  'NGC 1234',
  '12h 34m 56s, +12° 34\' 56"',
  '13:45:12.34, -23:45:12.34'
];

const results = inputs.map(input => {
  const result = sanitizer.sanitizeCoordinates(input);
  return {
    input,
    valid: result.isValid,
    output: result.coordinates,
    type: result.metadata?.inputFormat
  };
});

console.table(results);
```

### Integration with Telescope Control

```javascript
class TelescopeController {
  constructor() {
    this.sanitizer = new CoordinateSanitizer({
      outputFormat: 'aladin',
      validateRanges: true
    });
  }

  gotoTarget(target) {
    const result = this.sanitizer.sanitizeCoordinates(target);
    
    if (!result.isValid) {
      throw new Error(`Invalid target: ${result.error}`);
    }

    // Send to telescope
    this.sendToTelescope(result.coordinates);
    
    return {
      target: result.coordinates,
      inputType: result.metadata.inputFormat
    };
  }
}
```

### Error Handling

```javascript
const sanitizer = new CoordinateSanitizer();

function safeSearch(input) {
  try {
    const result = sanitizer.sanitizeCoordinates(input);
    
    if (!result.isValid) {
      return {
        success: false,
        error: result.error,
        suggestion: 'Please check coordinate format'
      };
    }

    return {
      success: true,
      coordinates: result.coordinates,
      inputType: result.metadata.inputFormat
    };
  } catch (error) {
    return {
      success: false,
      error: error.message,
      suggestion: 'Please contact support'
    };
  }
}
```

## Browser Usage

### Script Tag

```html
<script src="path/to/coordinate-sanitizer.js"></script>
<script>
  const sanitizer = new CoordinateSanitizer();
  const result = sanitizer.sanitizeCoordinates('M31');
  console.log(result.coordinates);
</script>
```

### ES Modules

The package ships a native ESM entry point, so `import` works out of the box in Node.js (`.mjs` files or `"type": "module"` projects) and in bundlers such as webpack, Rollup, and Vite:

```javascript
import CoordinateSanitizer from 'coordinate-sanitizer';

const sanitizer = new CoordinateSanitizer();
const result = sanitizer.sanitizeCoordinates('12h 34m 56s, +12° 34\' 56"');
```

## TypeScript Support

The library includes complete TypeScript definitions. Types are exposed in the `CoordinateSanitizer` namespace:

```typescript
import CoordinateSanitizer from 'coordinate-sanitizer';

const options: CoordinateSanitizer.CoordinateSanitizerOptions = {
  outputFormat: 'decimal',
  precision: 4,
  validateRanges: true
};

const sanitizer = new CoordinateSanitizer(options);
const result: CoordinateSanitizer.SanitizationResult = sanitizer.sanitizeCoordinates('M31');
```

## Performance

The library is optimized for high-performance applications:

- **10,000+ coordinates/second** on modern hardware
- **Zero dependencies** - no external libraries
- **Efficient regex patterns** for fast parsing
- **Minimal memory footprint**
- **Batch processing support**

## Security

The library includes built-in security features:

- **Input sanitization** prevents script injection
- **Malicious content detection** blocks dangerous patterns
- **Safe parsing** with input validation
- **No eval() or dynamic code execution**

## Error Handling

The library provides detailed error messages for debugging:

```javascript
const result = sanitizer.sanitizeCoordinates('invalid input');
if (!result.isValid) {
  console.log(`Error: ${result.error}`);
  // Handle error appropriately
}
```

Common error types:
- Invalid coordinate format
- Out of range values (RA > 24h, DEC > ±90°)
- Malformed input strings
- Security violations

## Testing

```bash
# Unit tests (67 tests)
npm test

# Integration tests – round-trip, cross-format consistency, known objects (33 tests)
npm run test:integration

# Full suite
npm run test:all

# Lint and type definitions check
npm run lint
npm run typecheck

# Run examples
npm run example
```

## Contributing

1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

## Development Setup

```bash
# Clone the repository
git clone https://github.com/Francescodib/coordinate-sanitizer.git
cd coordinate-sanitizer

# Install dependencies
npm install

# Run unit tests
npm test

# Run integration tests
npm run test:integration

# Run full suite
npm run test:all

# Run examples
npm run example
```

## Stats

- **Lines of code**: ~900
- **Unit tests**: 67
- **Integration tests**: 33 (round-trip, cross-format, known objects)
- **Performance**: 10,000+ coordinates/second
- **Bundle size**: Minimal (zero dependencies)
- **Formats supported**: 15+ input variations

## Changelog

### 2.0.0
- **Breaking**: bare decimal RA input is now interpreted as degrees (the documented astronomical convention), no longer as hours; the `decimal` output format emits RA in degrees. Use the new `raDecimalUnit: 'hours'` option to restore the previous behaviour
- Sexagesimal minutes/seconds components are now validated (`12h 99m 00s` is rejected)
- Native ESM entry point: `import CoordinateSanitizer from 'coordinate-sanitizer'` works in Node.js
- Fixed `exports` map ordering so TypeScript `node16`/`bundler` resolution finds the type definitions
- New labeled input format (`RA 05 35 17.30 DEC -05 23 28.00`) and new `parse()` method returning numeric values
- Rewritten TypeScript definitions (previous ones did not compile), ESLint and type checks added to CI
- Minimum Node.js version raised to 18

### 1.0.4
- Implemented `strictMode` option (was documented but had no effect)
- Fixed floating point carry overflow in `decimalToHMS` / `decimalToDMS` and in formatting functions
- Fixed `isValidFormat` to detect already-valid input for all output formats, not only `aladin`
- Fixed `formatHMSDMS` zero-padding for hours, minutes, and degree components
- `createPreset` now throws a descriptive `Error` for unknown preset names
- Updated TypeScript definitions: added `strictMode`, `createPreset` signature, fixed exports
- Added 29 integration tests (round-trip idempotency, cross-format consistency, known objects)

See [CHANGELOG.md](CHANGELOG.md) for the full history.

## License

MIT License - see [LICENSE](LICENSE) file for details.

## Related Projects

- [Aladin Lite](https://aladin.u-strasbg.fr/AladinLite/) - Sky atlas for the web
- [Astropy](https://www.astropy.org/) - Python astronomy library

## Links

- **npm Package**: https://www.npmjs.com/package/coordinate-sanitizer
- **Documentation**: https://github.com/Francescodib/coordinate-sanitizer#readme
- **Issues**: https://github.com/Francescodib/coordinate-sanitizer/issues
- **Discussions**: https://github.com/Francescodib/coordinate-sanitizer/discussions

## Support

- [Documentation](https://github.com/Francescodib/coordinate-sanitizer#readme)
- [Issue Tracker](https://github.com/Francescodib/coordinate-sanitizer/issues)
- [Discussions](https://github.com/Francescodib/coordinate-sanitizer/discussions)

## Author

**Francesco di Biase**
- GitHub: [@Francescodib](https://github.com/Francescodib)
- Project: [coordinate-sanitizer](https://github.com/Francescodib/coordinate-sanitizer)
